diff --git a/.gitignore b/.gitignore
index 9389d8f2c..7be24ad1d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,3 +30,4 @@ r2/srcount.pickle
r2/myproduction.ini
.DS_Store
r2/r2.egg-info/**
+r2/r2/public/static/sprite.png
diff --git a/r2/example.ini b/r2/example.ini
index f7ad061a1..8771d189f 100644
--- a/r2/example.ini
+++ b/r2/example.ini
@@ -9,7 +9,7 @@ template_debug = true
uncompressedJS = true
translator = true
sqlprinting = false
-
+exception_logging = false
log_start = true
proxy_addr =
@@ -30,6 +30,9 @@ adframetracker_url =
clicktracker_url =
traffic_url =
+# Just a list of words. Used by errlog.py to make up names for new errors.
+words_file = /usr/dict/words
+
# for sponsored links:
payment_domain = https://pay.localhost/
authorizenetname =
@@ -93,6 +96,9 @@ db_table_report_account_subreddit = relation, account, subreddit, main
db_table_award = thing, award
db_table_trophy = relation, account, award, award
+db_table_ad = thing, main
+db_table_adsr = relation, ad, subreddit, main
+
disallow_db_writes = False
###
@@ -103,6 +109,8 @@ timezone = UTC
lang = en
monitored_servers = localhost
+enable_usage_stats = false
+
#query cache settings
num_query_queue_workers = 0
query_queue_worker =
@@ -174,6 +182,10 @@ sr_dropdown_threshold = 15
smtp_server = localhost
new_link_share_delay = 5 minutes
+
+# email address of the person / people running your site
+nerds_email = root@localhost
+
share_reply = noreply@yourdomain.com
#user-agents to limit
diff --git a/r2/r2/config/middleware.py b/r2/r2/config/middleware.py
index a559b2c6d..fb72d0eaf 100644
--- a/r2/r2/config/middleware.py
+++ b/r2/r2/config/middleware.py
@@ -33,7 +33,7 @@ from pylons.wsgiapp import PylonsApp, PylonsBaseWSGIApp
from r2.config.environment import load_environment
from r2.config.rewrites import rewrites
-from r2.lib.utils import rstrips
+from r2.lib.utils import rstrips, is_authorized_cname
from r2.lib.jsontemplates import api_type
#middleware stuff
@@ -245,11 +245,10 @@ class DomainMiddleware(object):
auth_cnames = [x.strip() for x in auth_cnames.split(',')]
# we are going to be matching with endswith, so make sure there
# are no empty strings that have snuck in
- self.auth_cnames = [x for x in auth_cnames if x]
+ self.auth_cnames = filter(None, auth_cnames)
def is_auth_cname(self, domain):
- return any((domain == cname or domain.endswith('.' + cname))
- for cname in self.auth_cnames)
+ return is_authorized_cname(domain, self.auth_cnames)
def __call__(self, environ, start_response):
# get base domain as defined in INI file
diff --git a/r2/r2/config/routing.py b/r2/r2/config/routing.py
index 3442aa5ca..d7763f809 100644
--- a/r2/r2/config/routing.py
+++ b/r2/r2/config/routing.py
@@ -31,7 +31,7 @@ def make_map(global_conf={}, app_conf={}):
mc = map.connect
admin_routes.add(mc)
-
+
mc('/login', controller='front', action='login')
mc('/logout', controller='front', action='logout')
mc('/verify', controller='front', action='verify')
@@ -41,15 +41,16 @@ def make_map(global_conf={}, app_conf={}):
mc('/validuser', controller='front', action='validuser')
mc('/over18', controller='post', action='over18')
-
+
mc('/search', controller='front', action='search')
mc('/sup', controller='front', action='sup')
mc('/traffic', controller='front', action='site_traffic')
-
+
+ mc('/about/message/:where', controller='message', action='listing')
mc('/about/:location', controller='front',
action='editreddit', location = 'about')
-
+
mc('/reddits/create', controller='front', action='newreddit')
mc('/reddits/search', controller='front', action='search_reddits')
mc('/reddits/login', controller='front', action='login')
@@ -60,42 +61,51 @@ def make_map(global_conf={}, app_conf={}):
mc('/reddits/mine/:where', controller='myreddits', action='listing',
where='subscriber',
requirements=dict(where='subscriber|contributor|moderator'))
-
+
mc('/buttons', controller='buttons', action='button_demo_page')
#the frame
mc('/button_content', controller='buttons', action='button_content')
#/button.js and buttonlite.js - the embeds
- mc('/button', controller='buttons', action='button_embed')
+ mc('/button', controller='buttonjs', action='button_embed')
mc('/buttonlite', controller='buttons', action='button_lite')
-
+
mc('/widget', controller='buttons', action='widget_demo_page')
mc('/bookmarklets', controller='buttons', action='bookmarklets')
-
+
mc('/awards', controller='front', action='awards')
-
+
mc('/i18n', controller='feedback', action='i18n')
mc('/feedback', controller='feedback', action='feedback')
mc('/ad_inq', controller='feedback', action='ad_inq')
-
+
mc('/admin/i18n', controller='i18n', action='list')
mc('/admin/i18n/:action', controller='i18n')
mc('/admin/i18n/:action/:lang', controller='i18n')
+ mc('/admin/usage', controller='usage')
+
+ # Used for editing ads
+ mc('/admin/ads', controller='ads')
+ mc('/admin/ads/:adcn/:action', controller='ads',
+ requirements=dict(action="assign|srs"))
+
mc('/admin/awards', controller='awards')
mc('/admin/awards/:awardcn/:action', controller='awards',
requirements=dict(action="give|winners"))
+ mc('/admin/errors', controller='errorlog')
+
mc('/admin/:action', controller='admin')
-
+
mc('/user/:username/about', controller='user', action='about',
where='overview')
mc('/user/:username/:where', controller='user', action='listing',
where='overview')
-
+
mc('/prefs/:location', controller='front',
action='prefs', location='options')
-
+
mc('/info/0:article/*rest', controller = 'front',
action='oldinfo', dest='comments', type='ancient')
mc('/info/:article/:dest/:comment', controller='front',
@@ -113,7 +123,7 @@ def make_map(global_conf={}, app_conf={}):
action = 'comments', title=None, comment = None)
mc('/duplicates/:article/:title', controller = 'front',
action = 'duplicates', title=None)
-
+
mc('/mail/optout', controller='front', action = 'optout')
mc('/mail/optin', controller='front', action = 'optin')
mc('/stylesheet', controller = 'front', action = 'stylesheet')
@@ -138,8 +148,8 @@ def make_map(global_conf={}, app_conf={}):
mc('/shutdown', controller='health', action='shutdown')
mc('/', controller='hot', action='listing')
-
- listing_controllers = "hot|saved|toplinks|new|recommended|randomrising|comments"
+
+ listing_controllers = "hot|saved|new|recommended|randomrising|comments"
mc('/:controller', action='listing',
requirements=dict(controller=listing_controllers))
@@ -148,18 +158,20 @@ def make_map(global_conf={}, app_conf={}):
mc('/:sort', controller='browse', sort='top', action = 'listing',
requirements = dict(sort = 'top|controversial'))
-
+
mc('/message/compose', controller='message', action='compose')
mc('/message/messages/:mid', controller='message', action='listing',
where = "messages")
mc('/message/:where', controller='message', action='listing')
-
+ mc('/message/moderator/:subwhere', controller='message', action='listing',
+ where = 'moderator')
+
mc('/:action', controller='front',
requirements=dict(action="password|random|framebuster"))
mc('/:action', controller='embed',
requirements=dict(action="help|blog"))
mc('/help/*anything', controller='embed', action='help')
-
+
mc('/goto', controller='toolbar', action='goto')
mc('/tb/:id', controller='toolbar', action='tb')
mc('/toolbar/:action', controller='toolbar',
@@ -172,7 +184,7 @@ def make_map(global_conf={}, app_conf={}):
# additional toolbar-related rules just above the catchall
mc('/d/:what', controller='api', action='bookmarklet')
-
+
mc('/resetpassword/:key', controller='front',
action='resetpassword')
mc('/verification/:key', controller='front',
@@ -184,7 +196,7 @@ def make_map(global_conf={}, app_conf={}):
requirements=dict(action="login|reg"))
mc('/post/:action', controller='post',
requirements=dict(action="options|over18|unlogged_options|optout|optin|login|reg"))
-
+
mc('/api/distinguish/:how', controller='api', action="distinguish")
mc('/api/:action/:url_user', controller='api',
requirements=dict(action="login|register"))
@@ -193,7 +205,7 @@ def make_map(global_conf={}, app_conf={}):
mc('/api/:action', controller='promote',
requirements=dict(action="promote|unpromote|new_promo|link_thumb|freebie|promote_note|update_pay|refund|traffic_viewer|rm_traffic_viewer"))
mc('/api/:action', controller='api')
-
+
mc('/captcha/:iden', controller='captcha', action='captchaimg')
mc('/mediaembed/:link', controller="mediaembed", action="mediaembed")
@@ -202,17 +214,23 @@ def make_map(global_conf={}, app_conf={}):
mc('/store', controller='redirect', action='redirect',
dest='http://store.reddit.com/index.html')
-
+
mc('/code', controller='redirect', action='redirect',
dest='http://code.reddit.com/')
-
+
mc('/mobile', controller='redirect', action='redirect',
dest='http://m.reddit.com/')
mc('/authorize_embed', controller = 'front', action = 'authorize_embed')
-
- mc("/ads/", controller = "front", action = "ad")
- mc("/ads/:reddit", controller = "front", action = "ad")
+
+ # Used for showing ads
+ mc("/ads/", controller = "mediaembed", action = "ad")
+ mc("/ads/r/:reddit_name", controller = "mediaembed", action = "ad")
+ mc("/ads/:codename", controller = "mediaembed", action = "ad_by_codename")
+
+ mc('/comscore-iframe/', controller='mediaembed', action='comscore')
+ mc('/comscore-iframe/*url', controller='mediaembed', action='comscore')
+
# This route handles displaying the error page and
# graphics used in the 404/500
# error pages. It should likely stay at the top
diff --git a/r2/r2/controllers/__init__.py b/r2/r2/controllers/__init__.py
index bd02fd347..bb580fe5f 100644
--- a/r2/r2/controllers/__init__.py
+++ b/r2/r2/controllers/__init__.py
@@ -22,7 +22,6 @@
from listingcontroller import ListingController
from listingcontroller import HotController
from listingcontroller import SavedController
-from listingcontroller import ToplinksController
from listingcontroller import NewController
from listingcontroller import BrowseController
from listingcontroller import RecommendedController
@@ -39,6 +38,7 @@ from feedback import FeedbackController
from front import FrontController
from health import HealthController
from buttons import ButtonsController
+from buttons import ButtonjsController
from captcha import CaptchaController
from embed import EmbedController
from error import ErrorController
@@ -46,6 +46,9 @@ from post import PostController
from toolbar import ToolbarController
from i18n import I18nController
from awards import AwardsController
+from ads import AdsController
+from usage import UsageController
+from errorlog import ErrorlogController
from promotecontroller import PromoteController
from mediaembed import MediaembedController
diff --git a/r2/r2/controllers/ads.py b/r2/r2/controllers/ads.py
new file mode 100644
index 000000000..1b2ebb648
--- /dev/null
+++ b/r2/r2/controllers/ads.py
@@ -0,0 +1,56 @@
+# 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
+# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
+# License Version 1.1, but Sections 14 and 15 have been added to cover use of
+# 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-2010
+# CondeNet, Inc. All Rights Reserved.
+################################################################################
+from pylons import request, g
+from reddit_base import RedditController
+from r2.lib.pages import AdminPage, AdminAds, AdminAdAssign, AdminAdSRs
+from validator import *
+
+class AdsController(RedditController):
+
+ @validate(VSponsor())
+ def GET_index(self):
+ res = AdminPage(content = AdminAds(),
+ show_sidebar = False,
+ title = 'ads').render()
+ return res
+
+ @validate(VSponsor(),
+ ad = VAdByCodename('adcn'))
+ def GET_assign(self, ad):
+ if ad is None:
+ abort(404, 'page not found')
+
+ res = AdminPage(content = AdminAdAssign(ad),
+ show_sidebar = False,
+ title='assign an ad to a community').render()
+ return res
+
+ @validate(VSponsor(),
+ ad = VAdByCodename('adcn'))
+ def GET_srs(self, ad):
+ if ad is None:
+ abort(404, 'page not found')
+
+ res = AdminPage(content = AdminAdSRs(ad),
+ show_sidebar = False,
+ title='ad srs').render()
+ return res
diff --git a/r2/r2/controllers/api.py b/r2/r2/controllers/api.py
index 4942ae785..2bdad90dd 100644
--- a/r2/r2/controllers/api.py
+++ b/r2/r2/controllers/api.py
@@ -50,10 +50,25 @@ from r2.lib.media import force_thumbnail, thumbnail_url
from r2.lib.comment_tree import add_comment, delete_comment
from r2.lib import tracking, cssfilter, emailer
from r2.lib.subreddit_search import search_reddits
+from r2.lib.log import log_text
from datetime import datetime, timedelta
from md5 import md5
+def reject_vote(thing):
+ voteword = request.params.get('dir')
+
+ if voteword == '1':
+ voteword = 'upvote'
+ elif voteword == '0':
+ voteword = '0-vote'
+ elif voteword == '-1':
+ voteword = 'downvote'
+
+ log_text ("rejected vote", "Rejected %s from %s (%s) on %s %s via %s" %
+ (voteword, c.user.name, request.ip, thing.__class__.__name__,
+ thing._id36, request.referer), "info")
+
class ApiController(RedditController):
"""
Controller which deals with almost all AJAX site interaction.
@@ -73,6 +88,9 @@ class ApiController(RedditController):
return abort(404, 'not found')
links = link_from_url(request.params.get('url'), filter_spam = False)
+ if not links:
+ return abort(404, 'not found')
+
listing = wrap_links(links, num = count)
return BoringPage(_("API"), content = listing).render()
@@ -107,19 +125,19 @@ class ApiController(RedditController):
VUser(),
VModhash(),
ip = ValidIP(),
- to = VExistingUname('to'),
+ to = VMessageRecipent('to'),
subject = VRequired('subject', errors.NO_SUBJECT),
- body = VMessage(['text', 'message']))
+ body = VMarkdown(['text', 'message']))
def POST_compose(self, form, jquery, to, subject, body, ip):
"""
handles message composition under /message/compose.
"""
if not (form.has_errors("to", errors.USER_DOESNT_EXIST,
- errors.NO_USER) or
+ errors.NO_USER, errors.SUBREDDIT_NOEXIST) or
form.has_errors("subject", errors.NO_SUBJECT) or
form.has_errors("text", errors.NO_TEXT, errors.TOO_LONG) or
form.has_errors("captcha", errors.BAD_CAPTCHA)):
-
+
m, inbox_rel = Message._new(c.user, to, subject, body, ip)
form.set_html(".status", _("your message has been delivered"))
form.set_inputs(to = "", subject = "", text = "", captcha="")
@@ -128,20 +146,20 @@ class ApiController(RedditController):
@validatedForm(VUser(),
VCaptcha(),
- ValidDomain('url'),
VRatelimit(rate_user = True, rate_ip = True,
prefix = "rate_submit_"),
ip = ValidIP(),
sr = VSubmitSR('sr'),
url = VUrl(['url', 'sr']),
+ banmsg = VOkayDomain('url'),
title = VTitle('title'),
save = VBoolean('save'),
- selftext = VSelfText('text'),
+ selftext = VMarkdown('text'),
kind = VOneOf('kind', ['link', 'self', 'poll']),
then = VOneOf('then', ('tb', 'comments'),
default='comments'))
- def POST_submit(self, form, jquery, url, selftext, kind, title, save,
- sr, ip, then):
+ def POST_submit(self, form, jquery, url, banmsg, selftext, kind, title,
+ save, sr, ip, then):
#backwards compatability
if url == 'self':
kind = 'self'
@@ -178,6 +196,11 @@ class ApiController(RedditController):
elif form.has_errors("title", errors.NO_TEXT):
pass
+# Uncomment if we want to let spammers know we're on to them
+# if banmsg:
+# form.set_html(".field-url.BAD_URL", banmsg)
+# return
+
elif kind == 'self' and form.has_errors('text', errors.TOO_LONG):
pass
@@ -194,6 +217,9 @@ class ApiController(RedditController):
l = Link._submit(request.post.title, url if kind == 'link' else 'self',
c.user, sr, ip)
+ if banmsg:
+ admintools.spam(l, banner = "domain (%s)" % banmsg)
+
if kind == 'self':
l.url = l.make_permalink_slow()
l.is_self = True
@@ -264,31 +290,18 @@ class ApiController(RedditController):
rem = VBoolean('rem'),
reason = VReason('reason'))
def POST_login(self, form, jquery, user, username, dest, rem, reason):
+
if reason and reason[0] == 'redirect':
dest = reason[1]
- hc_key = "login_attempts-%s" % request.ip
-
- # TODO: You-know-what (not mentioning it, just in case
- # we accidentally release code with this comment in it)
-
- # Cache lifetime for login_attmempts
- la_expire_time = 3600 * 8
-
- recent_attempts = g.hardcache.add(hc_key, 0, time=la_expire_time)
-
- fake_failure = False
- if recent_attempts >= 25:
- g.log.error ("%s failed to login as %s (attempt #%d)"
- % (request.ip, username, recent_attempts))
- fake_failure = True
-
- if fake_failure or form.has_errors("passwd", errors.WRONG_PASSWORD):
+ if login_throttle(username, wrong_password = form.has_errors("passwd",
+ errors.WRONG_PASSWORD)):
VRatelimit.ratelimit(rate_ip = True, prefix = 'login_', seconds=1)
- g.hardcache.incr(hc_key, time = la_expire_time)
- else:
- self._login(form, user, dest, rem)
+ c.errors.add(errors.WRONG_PASSWORD, field = "passwd")
+
+ if not form.has_errors("passwd", errors.WRONG_PASSWORD):
+ self._login(form, user, dest, rem)
@validatedForm(VCaptcha(),
VRatelimit(rate_ip = True, prefix = "rate_register_"),
@@ -310,11 +323,11 @@ class ApiController(RedditController):
user = register(name, password)
VRatelimit.ratelimit(rate_ip = True, prefix = "rate_register_")
-
+
#anything else we know (email, languages)?
if email:
user.email = email
-
+
user.pref_lang = c.lang
if c.content_langs == 'all':
user.pref_content_langs = 'all'
@@ -322,10 +335,10 @@ class ApiController(RedditController):
langs = list(c.content_langs)
langs.sort()
user.pref_content_langs = tuple(langs)
-
+
d = c.user._dirties.copy()
user._commit()
-
+
c.user = user
if reason:
if reason[0] == 'redirect':
@@ -333,7 +346,7 @@ class ApiController(RedditController):
elif reason[0] == 'subscribe':
for sr, sub in reason[1].iteritems():
self._subscribe(sr, sub)
-
+
self._login(form, user, dest, rem)
@noresponse(VUser(),
@@ -530,7 +543,7 @@ class ApiController(RedditController):
if isinstance(thing, Link):
sr = thing.subreddit_slow
expire_hot(sr)
- queries.new_link(thing)
+ queries.delete_links(thing)
#comments have special delete tasks
elif isinstance(thing, Comment):
@@ -567,11 +580,11 @@ class ApiController(RedditController):
@validatedForm(VUser(),
VModhash(),
item = VByNameIfAuthor('thing_id'),
- text = VComment('text'))
+ text = VMarkdown('text'))
def POST_editusertext(self, form, jquery, item, text):
- if not form.has_errors("text",
- errors.NO_TEXT, errors.TOO_LONG,
- errors.NOT_AUTHOR):
+ if (not form.has_errors("text",
+ errors.NO_TEXT, errors.TOO_LONG) and
+ not form.has_errors("thing_id", errors.NOT_AUTHOR)):
if isinstance(item, Comment):
kind = 'comment'
@@ -580,7 +593,10 @@ class ApiController(RedditController):
kind = 'link'
item.selftext = text
- if (item._date < timeago('60 seconds')
+ if item._deleted:
+ return abort(403, "forbidden")
+
+ if (item._date < timeago('3 minutes')
or (item._ups + item._downs > 2)):
item.editted = True
@@ -601,7 +617,7 @@ class ApiController(RedditController):
prefix = "rate_comment_"),
ip = ValidIP(),
parent = VSubmitParent(['thing_id', 'parent']),
- comment = VComment(['text', 'comment']))
+ comment = VMarkdown(['text', 'comment']))
def POST_comment(self, commentform, jquery, parent, comment, ip):
should_ratelimit = True
#check the parent type here cause we need that for the
@@ -633,7 +649,8 @@ class ApiController(RedditController):
not commentform.has_errors("ratelimit",
errors.RATELIMIT) and
not commentform.has_errors("parent",
- errors.DELETED_COMMENT)):
+ errors.DELETED_COMMENT,
+ errors.DELETED_LINK)):
if is_message:
to = Account._byID(parent.author_id)
@@ -650,11 +667,9 @@ class ApiController(RedditController):
queries.queue_vote(c.user, item, True, ip,
cheater = (errors.CHEATER, None) in c.errors)
- #update last modified
- set_last_modified(link, 'comments')
-
- #update the comment cache
- add_comment(item)
+ # adding to comments-tree is done as part of
+ # newcomments_q, so if they refresh immediately they
+ # won't see their comment
# clean up the submission form and remove it from the DOM (if reply)
t = commentform.find("textarea")
@@ -666,7 +681,7 @@ class ApiController(RedditController):
# insert the new comment
jquery.insert_things(item)
-
+
# remove any null listings that may be present
jquery("#noresults").hide()
@@ -751,14 +766,12 @@ class ApiController(RedditController):
return
if vote_type == "rejected":
- g.log.error("POST_vote: rejected vote (%s) from '%s' on %s (%s)"%
- (request.params.get('dir'), c.user.name,
- thing._fullname, request.ip))
+ reject_vote(thing)
store = False
# TODO: temporary hack until we migrate the rest of the vote data
if thing._date < datetime(2009, 4, 17, 0, 0, 0, 0, g.tz):
- g.log.error("POST_vote: ignoring old vote on %s" % thing._fullname)
+ g.log.debug("POST_vote: ignoring old vote on %s" % thing._fullname)
store = False
# in a lock to prevent duplicate votes from people
@@ -982,21 +995,20 @@ class ApiController(RedditController):
name = VSubredditName("name"),
title = VLength("title", max_length = 100),
domain = VCnameDomain("domain"),
- description = VLength("description", max_length = 1000),
+ description = VMarkdown("description", max_length = 1000),
lang = VLang("lang"),
over_18 = VBoolean('over_18'),
allow_top = VBoolean('allow_top'),
show_media = VBoolean('show_media'),
+ use_whitelist = VBoolean('use_whitelist'),
type = VOneOf('type', ('public', 'private', 'restricted')),
ip = ValidIP(),
- ad_type = VOneOf('ad', ('default', 'basic', 'custom')),
- ad_file = VLength('ad-location', max_length = 500),
sponsor_text =VLength('sponsorship-text', max_length = 500),
sponsor_name =VLength('sponsorship-name', max_length = 500),
sponsor_url = VLength('sponsorship-url', max_length = 500),
css_on_cname = VBoolean("css_on_cname"),
)
- def POST_site_admin(self, form, jquery, name, ip, sr, ad_type, ad_file,
+ def POST_site_admin(self, form, jquery, name, ip, sr,
sponsor_text, sponsor_url, sponsor_name, **kw):
# the status button is outside the form -- have to reset by hand
form.parent().set_html('.status', "")
@@ -1005,7 +1017,7 @@ class ApiController(RedditController):
kw = dict((k, v) for k, v in kw.iteritems()
if k in ('name', 'title', 'domain', 'description', 'over_18',
'show_media', 'type', 'lang', "css_on_cname",
- 'allow_top'))
+ 'allow_top', 'use_whitelist'))
#if a user is banned, return rate-limit errors
if c.user._spam:
@@ -1054,10 +1066,6 @@ class ApiController(RedditController):
elif sr.is_moderator(c.user) or c.user_is_admin:
if c.user_is_admin:
- sr.ad_type = ad_type
- if ad_type != "custom":
- ad_file = Subreddit._defaults['ad_file']
- sr.ad_file = ad_file
sr.sponsorship_text = sponsor_text or ""
sr.sponsorship_url = sponsor_url or None
sr.sponsorship_name = sponsor_name or None
@@ -1137,50 +1145,58 @@ class ApiController(RedditController):
if r:
queries.new_savehide(r)
- @noresponse(VUser(),
- VModhash(),
- thing = VByName('id', multiple = True))
- def POST_collapse_message(self, thing):
- if not thing:
+ def collapse_handler(self, things, collapse):
+ if not things:
return
- for t in tup(thing):
+ things = tup(things)
+ srs = Subreddit._byID([t.sr_id for t in things if t.sr_id],
+ return_dict = True)
+ for t in things:
if hasattr(t, "to_id") and c.user._id == t.to_id:
- t.to_collapse = True
+ t.to_collapse = collapse
elif hasattr(t, "author_id") and c.user._id == t.author_id:
- t.author_collapse = True
+ t.author_collapse = collapse
+ elif isinstance(t, Message) and t.sr_id:
+ if srs[t.sr_id].is_moderator(c.user):
+ t.to_collapse = collapse
t._commit()
@noresponse(VUser(),
VModhash(),
- thing = VByName('id', multiple = True))
- def POST_uncollapse_message(self, thing):
+ things = VByName('id', multiple = True))
+ def POST_collapse_message(self, things):
+ self.collapse_handler(things, True)
+
+ @noresponse(VUser(),
+ VModhash(),
+ things = VByName('id', multiple = True))
+ def POST_uncollapse_message(self, things):
+ self.collapse_handler(things, False)
+
+ def unread_handler(self, thing, unread):
if not thing:
return
- for t in tup(thing):
- if hasattr(t, "to_id") and c.user._id == t.to_id:
- t.to_collapse = False
- elif hasattr(t, "author_id") and c.user._id == t.author_id:
- t.author_collapse = False
- t._commit()
+ # if the message has a recipient, try validating that
+ # desitination first (as it is cheaper and more common)
+ if not hasattr(thing, "to_id") or c.user._id == thing.to_id:
+ queries.set_unread(thing, c.user, unread)
+ # if the message is for a subreddit, check that next
+ if hasattr(thing, "sr_id"):
+ sr = thing.subreddit_slow
+ if sr and sr.is_moderator(c.user):
+ queries.set_unread(thing, sr, unread)
@noresponse(VUser(),
VModhash(),
thing = VByName('id'))
def POST_unread_message(self, thing):
- if not thing:
- return
- if hasattr(thing, "to_id") and c.user._id != thing.to_id:
- return
- queries.set_unread(thing, True)
+ self.unread_handler(thing, True)
@noresponse(VUser(),
VModhash(),
thing = VByName('id'))
def POST_read_message(self, thing):
- if not thing: return
- if hasattr(thing, "to_id") and c.user._id != thing.to_id:
- return
- queries.set_unread(thing, False)
+ self.unread_handler(thing, False)
@noresponse(VUser(),
VModhash(),
@@ -1203,10 +1219,14 @@ class ApiController(RedditController):
@validatedForm(VUser(),
parent = VByName('parent_id'))
def POST_moremessages(self, form, jquery, parent):
- if not parent.can_view():
+ if not parent.can_view_slow():
return self.abort(403,'forbidden')
- builder = MessageBuilder(c.user, parent = parent, skip = False)
+ if parent.sr_id:
+ builder = SrMessageBuilder(parent.subreddit_slow,
+ parent = parent, skip = False)
+ else:
+ builder = UserMessageBuilder(c.user, parent = parent, skip = False)
listing = Listing(builder).listing()
a = []
for item in listing.things:
@@ -1221,19 +1241,18 @@ class ApiController(RedditController):
@validatedForm(link = VByName('link_id'),
sort = VMenu('where', CommentSortMenu),
children = VCommentIDs('children'),
- depth = VInt('depth', min = 0, max = 8),
mc_id = nop('id'))
def POST_morechildren(self, form, jquery,
- link, sort, children, depth, mc_id):
+ link, sort, children, mc_id):
user = c.user if c.user_is_loggedin else None
if not link or not link.subreddit_slow.can_view(user):
- return self.abort(403,'forbidden')
+ return abort(403,'forbidden')
if children:
builder = CommentBuilder(link, CommentSortMenu.operator(sort),
children)
listing = Listing(builder, nextprev = False)
- items = listing.get_items(starting_depth = depth, num = 20)
+ items = listing.get_items(num = 20)
def _children(cur_items):
items = []
for cm in cur_items:
@@ -1399,6 +1418,107 @@ class ApiController(RedditController):
tr._is_enabled = True
+ @validatedForm(VAdmin(),
+ hexkey=VLength("hexkey", max_length=32),
+ nickname=VLength("nickname", max_length = 1000),
+ status = VOneOf("status",
+ ("new", "severe", "interesting", "normal", "fixed")))
+ def POST_edit_error(self, form, jquery, hexkey, nickname, status):
+ if form.has_errors(("hexkey", "nickname", "status"),
+ errors.NO_TEXT, errors.INVALID_OPTION):
+ pass
+
+ if form.has_error():
+ return
+
+ key = "error_nickname-%s" % str(hexkey)
+ g.hardcache.set(key, nickname, 86400 * 365)
+
+ key = "error_status-%s" % str(hexkey)
+ g.hardcache.set(key, status, 86400 * 365)
+
+ form.set_html(".status", _('saved'))
+
+ @validatedForm(VSponsor(),
+ ad = VByName("fullname"),
+ colliding_ad=VAdByCodename(("codename", "fullname")),
+ codename = VLength("codename", max_length = 100),
+ imgurl = VLength("imgurl", max_length = 1000),
+ linkurl = VLength("linkurl", max_length = 1000))
+ def POST_editad(self, form, jquery, ad, colliding_ad, codename,
+ imgurl, linkurl):
+ if form.has_errors(("codename", "imgurl", "linkurl"),
+ errors.NO_TEXT):
+ pass
+
+ if form.has_errors(("codename"), errors.INVALID_OPTION):
+ form.set_html(".status", "some other ad has that codename")
+ pass
+
+ if form.has_error():
+ return
+
+ if ad is None:
+ Ad._new(codename, imgurl, linkurl)
+ form.set_html(".status", "saved. reload to see it.")
+ return
+
+ ad.codename = codename
+ ad.imgurl = imgurl
+ ad.linkurl = linkurl
+ ad._commit()
+ form.set_html(".status", _('saved'))
+
+ @validatedForm(VSponsor(),
+ ad = VByName("fullname"),
+ sr = VSubmitSR("community"),
+ weight = VInt("weight",
+ coerce=False, min=0, max=100000),
+ )
+ def POST_assignad(self, form, jquery, ad, sr, weight):
+ if form.has_errors("ad", errors.NO_TEXT):
+ pass
+
+ if form.has_errors("community", errors.SUBREDDIT_REQUIRED,
+ errors.SUBREDDIT_NOEXIST, errors.SUBREDDIT_NOTALLOWED):
+ pass
+
+ if form.has_errors("fullname", errors.NO_TEXT):
+ pass
+
+ if form.has_errors("weight", errors.BAD_NUMBER):
+ pass
+
+ if form.has_error():
+ return
+
+ if ad.codename == "DART" and sr.name == g.default_sr and weight != 100:
+ log_text("Bad default DART weight",
+ "The default DART weight can only be 100, not %s."
+ % weight,
+ "error")
+ abort(403, 'forbidden')
+
+ existing = AdSR.by_ad_and_sr(ad, sr)
+
+ if weight is not None:
+ if existing:
+ existing.weight = weight
+ existing._commit()
+ else:
+ AdSR._new(ad, sr, weight)
+
+ form.set_html(".status", _('saved'))
+
+ else:
+ if existing:
+ existing._delete()
+ AdSR.by_ad(ad, _update=True)
+ AdSR.by_sr(sr, _update=True)
+
+ form.set_html(".status", _('deleted'))
+
+
@validatedForm(VAdmin(),
award = VByName("fullname"),
colliding_award=VAwardByCodename(("codename", "fullname")),
@@ -1448,10 +1568,8 @@ class ApiController(RedditController):
if form.has_errors("award", errors.NO_TEXT):
pass
- if form.has_errors("recipient", errors.USER_DOESNT_EXIST):
- pass
-
- if form.has_errors("recipient", errors.NO_USER):
+ if form.has_errors("recipient", errors.USER_DOESNT_EXIST,
+ errors.NO_USER):
pass
if form.has_errors("fullname", errors.NO_TEXT):
@@ -1488,6 +1606,7 @@ class ApiController(RedditController):
return self.abort404()
recipient = trophy._thing1
award = trophy._thing2
+
trophy._delete()
Trophy.by_account(recipient, _update=True)
Trophy.by_award(award, _update=True)
@@ -1566,7 +1685,7 @@ class ApiController(RedditController):
"%s_%s" % (s._fullname, s.sponsorship_name))
- @json_validate(query = nop('query'))
+ @json_validate(query = VPrintable('query', max_length = 50))
def POST_search_reddit_names(self, query):
names = []
if query:
diff --git a/r2/r2/controllers/buttons.py b/r2/r2/controllers/buttons.py
index df99e0930..34f1b1995 100644
--- a/r2/r2/controllers/buttons.py
+++ b/r2/r2/controllers/buttons.py
@@ -19,7 +19,7 @@
# All portions of the code written by CondeNet are Copyright (c) 2006-2010
# CondeNet, Inc. All Rights Reserved.
################################################################################
-from reddit_base import RedditController
+from reddit_base import RedditController, MinimalController, make_key
from r2.lib.pages import Button, ButtonNoBody, ButtonEmbed, ButtonLite, \
ButtonDemoPanel, WidgetDemoPanel, Bookmarklets, BoringPage
from r2.lib.pages.things import wrap_links
@@ -31,6 +31,55 @@ from pylons.i18n import _
from r2.lib.filters import spaceCompress
from r2.controllers.listingcontroller import ListingController
+class ButtonjsController(MinimalController):
+ def pre(self):
+ MinimalController.pre(self)
+ # override user loggedin behavior to ensure this page always
+ # uses the page cache
+ (user, maybe_admin) = \
+ valid_cookie(c.cookies[g.login_cookie].value
+ if g.login_cookie in c.cookies
+ else '')
+ if user:
+ self.user_is_loggedin = True
+
+ @validate(buttontype = VInt('t', 1, 5),
+ url = VSanitizedUrl("url"),
+ _height = VInt('height', 0, 300),
+ _width = VInt('width', 0, 800),
+ autohide = VBoolean("autohide"))
+ def GET_button_embed(self, buttontype, _height, _width, url, autohide):
+ # no buttons on domain listings
+ if isinstance(c.site, DomainSR):
+ return self.abort404()
+ c.render_style = 'js'
+ c.response_content_type = 'text/javascript; charset=UTF-8'
+ if not c.user_is_loggedin and autohide:
+ c.response.content = "void(0);"
+ return c.response
+
+ buttontype = buttontype or 1
+ width, height = ((120, 22), (51, 69), (69, 52),
+ (51, 52), (600, 52))[min(buttontype - 1, 4)]
+ if _width: width = _width
+ if _height: height = _height
+
+ bjs = ButtonEmbed(button=buttontype,
+ width=width,
+ height=height,
+ url = url,
+ referer = request.referer).render()
+ return self.sendjs(bjs, callback='', escape=False)
+
+ def request_key(self):
+ return make_key('button_request_key',
+ c.lang,
+ c.content_langs,
+ request.host,
+ c.cname,
+ request.referer,
+ request.fullpath)
+
class ButtonsController(RedditController):
def buttontype(self):
b = request.get.get('t') or 1
@@ -83,7 +132,6 @@ class ButtonsController(RedditController):
width = VInt('width', 0, 800),
l = VByName('id'))
def GET_button_content(self, url, title, css, vote, newwindow, width, l):
-
# no buttons on domain listings
if isinstance(c.site, DomainSR):
c.site = Default
@@ -108,12 +156,9 @@ class ButtonsController(RedditController):
button = self.buttontype(), **kw)
l = self.get_wrapped_link(url, l, wrapper)
- res = l.render()
- c.response.content = spaceCompress(res)
- return c.response
+ return l.render()
-
@validate(buttontype = VInt('t', 1, 5),
url = VSanitizedUrl("url"),
_height = VInt('height', 0, 300),
@@ -191,5 +236,3 @@ class ButtonsController(RedditController):
return BoringPage(_("bookmarklets"),
show_sidebar = False,
content=Bookmarklets()).render()
-
-
diff --git a/r2/r2/controllers/error.py b/r2/r2/controllers/error.py
index b155e4687..c48942153 100644
--- a/r2/r2/controllers/error.py
+++ b/r2/r2/controllers/error.py
@@ -32,7 +32,7 @@ from r2.lib.filters import safemarkdown, unsafe
try:
# place all r2 specific imports in here. If there is a code error, it'll get caught and
# the stack trace won't be presented to the user in production
- from reddit_base import RedditController
+ from reddit_base import RedditController, Cookies
from r2.models.subreddit import Default, Subreddit
from r2.models.link import Link
from r2.lib import pages
@@ -122,7 +122,7 @@ class ErrorController(RedditController):
c.site.name)
message = (strings.banned_subreddit %
dict(link = '/message/compose?to=%s&subject=%s' %
- (g.admin_message_acct,
+ (url_escape(g.admin_message_acct),
url_escape(subject))))
res = pages.RedditError(_('this reddit has been banned'),
@@ -146,8 +146,8 @@ class ErrorController(RedditController):
def GET_document(self):
try:
- #no cookies on errors
- c.cookies.clear()
+ # clear cookies the old fashioned way
+ c.cookies = Cookies()
code = request.GET.get('code', '')
srname = request.GET.get('srname', '')
@@ -155,7 +155,7 @@ class ErrorController(RedditController):
if srname:
c.site = Subreddit._by_name(srname)
if c.render_style not in self.allowed_render_styles:
- return str(code)
+ return str(int(code))
elif takedown and code == '404':
link = Link._by_fullname(takedown)
return pages.TakedownPage(link).render()
diff --git a/r2/r2/controllers/errorlog.py b/r2/r2/controllers/errorlog.py
new file mode 100644
index 000000000..d11f0c100
--- /dev/null
+++ b/r2/r2/controllers/errorlog.py
@@ -0,0 +1,34 @@
+# 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
+# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
+# License Version 1.1, but Sections 14 and 15 have been added to cover use of
+# 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-2010
+# CondeNet, Inc. All Rights Reserved.
+################################################################################
+from pylons import request, g
+from reddit_base import RedditController
+from r2.lib.pages import AdminPage, AdminErrorLog
+from validator import *
+
+class ErrorlogController(RedditController):
+ @validate(VAdmin())
+ def GET_index(self):
+ res = AdminPage(content = AdminErrorLog(),
+ title = 'error log',
+ show_sidebar = False
+ ).render()
+ return res
diff --git a/r2/r2/controllers/errors.py b/r2/r2/controllers/errors.py
index 527a346e5..28bcacf10 100644
--- a/r2/r2/controllers/errors.py
+++ b/r2/r2/controllers/errors.py
@@ -24,8 +24,8 @@ from pylons.i18n import _
from copy import copy
error_list = dict((
- ('USER_REQUIRED', _("please login to do that")),
- ('VERIFIED_USER_REQUIRED', _("you need to set a valid email address to do that.")),
+ ('USER_REQUIRED', _("please login to do that")),
+ ('VERIFIED_USER_REQUIRED', _("you need to set a valid email address to do that.")),
('NO_URL', _('a url is required')),
('BAD_URL', _('you should check that url')),
('BAD_CAPTCHA', _('care to try these again?')),
@@ -33,9 +33,10 @@ error_list = dict((
('USERNAME_TAKEN', _('that username is already taken')),
('NO_THING_ID', _('id not specified')),
('NOT_AUTHOR', _("you can't do that")),
+ ('DELETED_LINK', _('the link you are commenting on has been deleted')),
('DELETED_COMMENT', _('that comment has been deleted')),
- ('DELETED_THING', _('that element has been deleted.')),
- ('BAD_PASSWORD', _('invalid password')),
+ ('DELETED_THING', _('that element has been deleted')),
+ ('BAD_PASSWORD', _('that password is unacceptable')),
('WRONG_PASSWORD', _('invalid password')),
('BAD_PASSWORD_MATCH', _('passwords do not match')),
('NO_NAME', _('please enter a name')),
@@ -47,6 +48,7 @@ error_list = dict((
('NO_USER', _('please enter a username')),
('INVALID_PREF', "that preference isn't valid"),
('BAD_NUMBER', _("that number isn't in the right range (%(min)d to %(max)d)")),
+ ('BAD_STRING', _("you used a character here that we can't handle")),
('BAD_BID', _("your bid must be at least $%(min)d per day and no more than to $%(max)d in total.")),
('ALREADY_SUB', _("that link has already been submitted")),
('SUBREDDIT_EXISTS', _('that reddit already exists')),
@@ -58,7 +60,6 @@ error_list = dict((
('EXPIRED', _('your session has expired')),
('DRACONIAN', _('you must accept the terms first')),
('BANNED_IP', "IP banned"),
- ('BANNED_DOMAIN', "Domain banned"),
('BAD_CNAME', "that domain isn't going to work"),
('USED_CNAME', "that domain is already in use"),
('INVALID_OPTION', _('that option is not valid')),
diff --git a/r2/r2/controllers/front.py b/r2/r2/controllers/front.py
index 212e69736..362d159d9 100644
--- a/r2/r2/controllers/front.py
+++ b/r2/r2/controllers/front.py
@@ -89,13 +89,15 @@ class FrontController(RedditController):
else:
links = list(links)[:g.num_serendipity]
+ rand.shuffle(links)
+
builder = IDBuilder(links, skip = True,
keep_fn = lambda x: x.fresh,
- num = g.num_serendipity)
+ num = 1)
links = builder.get_items()[0]
if links:
- l = rand.choice(links)
+ l = links[0]
return self.redirect(add_sr("/tb/" + l._id36))
else:
return self.redirect(add_sr('/'))
@@ -274,8 +276,12 @@ class FrontController(RedditController):
content.append(FriendList())
elif location == 'update':
content = PrefUpdate()
+ elif location == 'feeds' and c.user.pref_private_feeds:
+ content = PrefFeeds()
elif location == 'delete':
content = PrefDelete()
+ else:
+ return self.abort404()
return PrefsPage(content = content, infotext=infotext).render()
@@ -310,7 +316,7 @@ class FrontController(RedditController):
# moderator is either reddit's moderator or an admin
is_moderator = c.user_is_loggedin and c.site.is_moderator(c.user) or c.user_is_admin
-
+ extension_handling = False
if is_moderator and location == 'edit':
pane = PaneStack()
if created == 'true':
@@ -320,8 +326,11 @@ class FrontController(RedditController):
pane = ModList(editable = is_moderator)
elif is_moderator and location == 'banned':
pane = BannedList(editable = is_moderator)
- elif location == 'contributors' and c.site.type != 'public':
- pane = ContributorList(editable = is_moderator)
+ elif (location == 'contributors' and
+ (c.site.type != 'public' or
+ (c.user_is_loggedin and c.site.use_whitelist and
+ (c.site.is_moderator(c.user) or c.user_is_admin)))):
+ pane = ContributorList(editable = is_moderator)
elif (location == 'stylesheet'
and c.site.can_change_stylesheet(c.user)
and not g.css_killswitch):
@@ -338,18 +347,35 @@ class FrontController(RedditController):
else c.site.get_spam())
builder_cls = (QueryBuilder if isinstance(query, thing.Query)
else IDBuilder)
+ def keep_fn(x):
+ # no need to bother mods with banned users, or deleted content
+ if x.hidden or x._deleted:
+ return False
+ if location == "reports" and not x._spam:
+ return (x.reported > 0)
+ if location == "spam":
+ return x._spam
+ return True
+
builder = builder_cls(query,
+ skip = True,
num = num, after = after,
+ keep_fn = keep_fn,
count = count, reverse = reverse,
wrap = ListingController.builder_wrapper)
listing = LinkListing(builder)
pane = listing.listing()
+ if c.user.pref_private_feeds:
+ extension_handling = "private"
elif is_moderator and location == 'traffic':
pane = RedditTraffic()
+ elif c.user_is_sponsor and location == 'ads':
+ pane = RedditAds()
else:
return self.abort404()
- return EditReddit(content = pane).render()
+ return EditReddit(content = pane,
+ extension_handling = extension_handling).render()
def GET_awards(self):
"""The awards page."""
@@ -517,51 +543,50 @@ class FrontController(RedditController):
return builder.total_num, timing, res
- def GET_login(self):
+ @validate(dest = VDestination())
+ def GET_login(self, dest):
"""The /login form. No link to this page exists any more on
the site (all actions invoking it now go through the login
cover). However, this page is still used for logging the user
in during submission or voting from the bookmarklets."""
- # dest is the location to redirect to upon completion
- dest = request.get.get('dest','') or request.referer or '/'
if (c.user_is_loggedin and
not request.environ.get('extension') == 'embed'):
return self.redirect(dest)
return LoginPage(dest = dest).render()
- def GET_logout(self):
- dest = request.referer or '/'
+ @validate(VUser(),
+ VModhash(),
+ dest = VDestination())
+ def GET_logout(self, dest):
return self.redirect(dest)
@validate(VUser(),
- VModhash())
- def POST_logout(self, dest = None):
+ VModhash(),
+ dest = VDestination())
+ def POST_logout(self, dest):
"""wipe login cookie and redirect to referer."""
self.logout()
- dest = request.post.get('dest','') or request.referer or '/'
return self.redirect(dest)
-
- @validate(VUser())
- def GET_adminon(self):
+
+ @validate(VUser(),
+ dest = VDestination())
+ def GET_adminon(self, dest):
"""Enable admin interaction with site"""
#check like this because c.user_is_admin is still false
if not c.user.name in g.admins:
return self.abort404()
self.login(c.user, admin = True)
-
- dest = request.referer or '/'
return self.redirect(dest)
- @validate(VAdmin())
- def GET_adminoff(self):
+ @validate(VAdmin(),
+ dest = VDestination())
+ def GET_adminoff(self, dest):
"""disable admin interaction with site."""
if not c.user.name in g.admins:
return self.abort404()
self.login(c.user, admin = False)
-
- dest = request.referer or '/'
return self.redirect(dest)
def GET_validuser(self):
@@ -604,9 +629,9 @@ class FrontController(RedditController):
captcha = Captcha() if c.user.needs_captcha() else None
sr_names = (Subreddit.submit_sr_names(c.user) or
Subreddit.submit_sr_names(None))
-
- return FormPage(_("submit"),
+ return FormPage(_("submit"),
+ show_sidebar = True,
content=NewLink(url=url or '',
title=title or '',
subreddits = sr_names,
@@ -714,7 +739,3 @@ class FrontController(RedditController):
def GET_site_traffic(self):
return BoringPage("traffic",
content = RedditTraffic()).render()
-
-
- def GET_ad(self, reddit = None):
- return Dart_Ad(reddit).render(style="html")
diff --git a/r2/r2/controllers/health.py b/r2/r2/controllers/health.py
index ce98150f7..0668245c0 100644
--- a/r2/r2/controllers/health.py
+++ b/r2/r2/controllers/health.py
@@ -8,6 +8,8 @@ from pylons import c, g
from reddit_base import RedditController
from r2.lib.amqp import worker
+from validator import *
+
class HealthController(RedditController):
def shutdown(self):
thread_pool = c.thread_pool
@@ -40,9 +42,12 @@ class HealthController(RedditController):
c.response.content = "i'm still alive!"
return c.response
- def GET_shutdown(self):
- if not g.allow_shutdown:
+ @validate(secret=nop('secret'))
+ def GET_shutdown(self, secret):
+ if not g.shutdown_secret:
self.abort404()
+ if not secret or secret != g.shutdown_secret:
+ self.abort403()
c.dontcache = True
#the will make the next health-check initiate the shutdown
diff --git a/r2/r2/controllers/listingcontroller.py b/r2/r2/controllers/listingcontroller.py
index 23ca47d31..81a8a67be 100644
--- a/r2/r2/controllers/listingcontroller.py
+++ b/r2/r2/controllers/listingcontroller.py
@@ -122,7 +122,7 @@ class ListingController(RedditController):
builder_cls = SearchBuilder
elif isinstance(self.query_obj, iters):
builder_cls = IDBuilder
- elif isinstance(self.query_obj, queries.CachedResults):
+ elif isinstance(self.query_obj, (queries.CachedResults, queries.MergedCachedResults)):
builder_cls = IDBuilder
b = builder_cls(self.query_obj,
@@ -253,7 +253,7 @@ class HotController(FixListing, ListingController):
and not isinstance(c.site, FakeSubreddit)
and self.after is None
and self.count == 0):
- return get_hot(c.site, only_fullnames = True)
+ return get_hot([c.site], only_fullnames = True)[0]
else:
return c.site.get_links('hot', 'all')
@@ -286,16 +286,6 @@ class SavedController(ListingController):
def GET_listing(self, **env):
return ListingController.GET_listing(self, **env)
-class ToplinksController(ListingController):
- where = 'toplinks'
- title_text = _('top scoring links')
-
- def query(self):
- return c.site.get_links('toplinks', 'all')
-
- def GET_listing(self, **env):
- return ListingController.GET_listing(self, **env)
-
class NewController(ListingController):
where = 'new'
title_text = _('newest submissions')
@@ -503,11 +493,12 @@ class UserController(ListingController):
class MessageController(ListingController):
show_sidebar = False
+ show_nums = False
render_cls = MessagePage
@property
def menus(self):
- if self.where in ('inbox', 'messages', 'comments',
+ if c.default_sr and self.where in ('inbox', 'messages', 'comments',
'selfreply', 'unread'):
buttons = (NavButton(_("all"), "inbox"),
NavButton(_("unread"), "unread"),
@@ -517,12 +508,27 @@ class MessageController(ListingController):
return [NavMenu(buttons, base_path = '/message/',
default = 'inbox', type = "flatlist")]
+ elif not c.default_sr or self.where == 'moderator':
+ buttons = (NavButton(_("all"), "inbox"),
+ NavButton(_("unread"), "unread"))
+ return [NavMenu(buttons, base_path = '/message/moderator/',
+ default = 'inbox', type = "flatlist")]
return []
def title(self):
return _('messages') + ': ' + _(self.where)
+ def keep_fn(self):
+ def keep(item):
+ wouldkeep = item.keep_item(item)
+ # don't show user their own unread stuff
+ if ((self.where == 'unread' or self.subwhere == 'unread')
+ and item.author_id == c.user._id):
+ return False
+ return wouldkeep
+ return keep
+
@staticmethod
def builder_wrapper(thing):
if isinstance(thing, Comment):
@@ -539,24 +545,32 @@ class MessageController(ListingController):
return w
def builder(self):
- if self.where == 'messages':
+ if (self.where == 'messages' or
+ (self.where == "moderator" and self.subwhere != "unread")):
+ root = c.user
+ message_cls = UserMessageBuilder
+ if not c.default_sr:
+ root = c.site
+ message_cls = SrMessageBuilder
+ elif self.where == 'moderator' and self.subwhere != 'unread':
+ message_cls = ModeratorMessageBuilder
+
+ parent = None
+ skip = False
if self.message:
if self.message.first_message:
parent = Message._byID(self.message.first_message)
else:
parent = self.message
- return MessageBuilder(c.user, parent = parent,
- skip = False,
- focal = self.message,
- wrap = self.builder_wrapper,
- num = self.num)
elif c.user.pref_threaded_messages:
skip = (c.render_style == "html")
- return MessageBuilder(c.user, wrap = self.builder_wrapper,
- skip = skip,
- num = self.num,
- after = self.after,
- reverse = self.reverse)
+
+ return message_cls(root, wrap = self.builder_wrapper,
+ parent = parent,
+ skip = skip,
+ num = self.num,
+ after = self.after,
+ reverse = self.reverse)
return ListingController.builder(self)
def listing(self):
@@ -578,7 +592,22 @@ class MessageController(ListingController):
q = queries.get_unread_inbox(c.user)
elif self.where == 'sent':
q = queries.get_sent(c.user)
-
+ elif self.where == 'moderator' and self.subwhere == 'unread':
+ if c.default_sr:
+ srids = Subreddit.reverse_moderator_ids(c.user)
+ srs = Subreddit._byID(srids, data = False, return_dict = False)
+ q = queries.merge_results(
+ *[queries.get_unread_subreddit_messages(s) for s in srs])
+ else:
+ q = queries.get_unread_subreddit_messages(c.site)
+ elif self.where == 'moderator':
+ if c.have_mod_messages and self.mark != 'false':
+ c.user.modmsgtime = False
+ c.user._commit()
+ # the query is handled by the builder on the moderator page
+ return
+ else:
+ return self.abort404()
if self.where != 'sent':
#reset the inbox
if c.have_messages and self.mark != 'false':
@@ -590,11 +619,16 @@ class MessageController(ListingController):
@validate(VUser(),
message = VMessageID('mid'),
mark = VOneOf('mark',('true','false'), default = 'true'))
- def GET_listing(self, where, mark, message, **env):
- self.where = where
+ def GET_listing(self, where, mark, message, subwhere = None, **env):
+ if not (c.default_sr or c.site.is_moderator(c.user) or c.user_is_admin):
+ abort(403, "forbidden")
+ if not c.default_sr:
+ self.where = "moderator"
+ else:
+ self.where = where
+ self.subwhere = subwhere
self.mark = mark
self.message = message
- c.msg_location = where
return ListingController.GET_listing(self, **env)
@validate(VUser(),
@@ -609,7 +643,7 @@ class MessageController(ListingController):
message = message,
success = success)
return MessagePage(content = content).render()
-
+
class RedditsController(ListingController):
render_cls = SubredditsPage
@@ -645,7 +679,8 @@ class MyredditsController(ListingController):
NavButton(plurals.contributor, 'contributor'),
NavButton(plurals.moderator, 'moderator'))
- return [NavMenu(buttons, base_path = '/reddits/mine/', default = 'subscriber', type = "flatlist")]
+ return [NavMenu(buttons, base_path = '/reddits/mine/',
+ default = 'subscriber', type = "flatlist")]
def title(self):
return _('reddits: ') + self.where
diff --git a/r2/r2/controllers/mediaembed.py b/r2/r2/controllers/mediaembed.py
index 9cb4c5cf6..c2e30a11f 100644
--- a/r2/r2/controllers/mediaembed.py
+++ b/r2/r2/controllers/mediaembed.py
@@ -20,23 +20,24 @@
# CondeNet, Inc. All Rights Reserved.
################################################################################
from validator import *
-from reddit_base import RedditController
+from reddit_base import MinimalController
from r2.lib.scraper import scrapers
-from r2.lib.pages import MediaEmbedBody
+from r2.lib.pages import MediaEmbedBody, ComScore, render_ad
from pylons import request
+from pylons.controllers.util import abort
-class MediaembedController(RedditController):
+class MediaembedController(MinimalController):
@validate(link = VLink('link'))
def GET_mediaembed(self, link):
if request.host != g.media_domain:
# don't serve up untrusted content except on our
# specifically untrusted domain
- return self.abort404()
+ abort(404)
if not link or not link.media_object:
- return self.abort404()
+ abort(404)
if isinstance(link.media_object, basestring):
# it's an old-style string
@@ -50,3 +51,16 @@ class MediaembedController(RedditController):
content = media_embed.content
return MediaEmbedBody(body = content).render()
+
+ def GET_ad(self, reddit_name = None):
+ c.render_style = "html"
+ return render_ad(reddit_name=reddit_name)
+
+ def GET_ad_by_codename(self, codename = None):
+ if not codename:
+ abort(404)
+ c.render_style = "html"
+ return render_ad(codename=codename)
+
+ def GET_comscore(self, reddit = None):
+ return ComScore().render(style="html")
diff --git a/r2/r2/controllers/post.py b/r2/r2/controllers/post.py
index 5736b7631..130ac3754 100644
--- a/r2/r2/controllers/post.py
+++ b/r2/r2/controllers/post.py
@@ -29,29 +29,10 @@ from pylons.i18n import _
from r2.models import *
import sha
-def to_referer(func, **params):
- def _to_referer(self, *a, **kw):
- res = func(self, *a, **kw)
- dest = res.get('redirect') or request.referer or '/'
- return self.redirect(dest + query_string(params))
- return _to_referer
-
-
class PostController(ApiController):
def api_wrapper(self, kw):
return Storage(**kw)
-#TODO: feature disabled for now
-# @to_referer
-# @validate(VUser(),
-# key = VOneOf('key', ('pref_bio','pref_location',
-# 'pref_url')),
-# value = nop('value'))
-# def POST_user_desc(self, key, value):
-# setattr(c.user, key, value)
-# c.user._commit()
-# return {}
-
def set_options(self, all_langs, pref_lang, **kw):
if c.errors.errors:
print "fucker"
@@ -87,7 +68,9 @@ class PostController(ApiController):
self.set_options( all_langs, pref_lang)
return self.redirect(request.referer)
- @validate(pref_frame = VBoolean('frame'),
+ @validate(VUser(),
+ VModhash(),
+ pref_frame = VBoolean('frame'),
pref_clickgadget = VBoolean('clickgadget'),
pref_organic = VBoolean('organic'),
pref_newwindow = VBoolean('newwindow'),
@@ -110,6 +93,7 @@ class PostController(ApiController):
pref_mark_messages_read = VBoolean("mark_messages_read"),
pref_threaded_messages = VBoolean("threaded_messages"),
pref_collapse_read_messages = VBoolean("collapse_read_messages"),
+ pref_private_feeds = VBoolean("private_feeds"),
all_langs = nop('all-langs', default = 'all'))
def POST_options(self, all_langs, pref_lang, **kw):
#temporary. eventually we'll change pref_clickgadget to an
@@ -176,12 +160,12 @@ class PostController(ApiController):
msg_hash = msg_hash)).render()
- def POST_login(self, *a, **kw):
+ @validate(dest = VDestination(default = "/"))
+ def POST_login(self, dest, *a, **kw):
ApiController.POST_login(self, *a, **kw)
c.render_style = "html"
c.response_content_type = ""
- dest = request.post.get('dest', request.referer or '/')
errors = list(c.errors)
if errors:
for e in errors:
@@ -190,18 +174,17 @@ class PostController(ApiController):
c.errors.remove(e)
c.errors.add(e[0], msg)
- dest = request.post.get('dest', request.referer or '/')
return LoginPage(user_login = request.post.get('user'),
dest = dest).render()
return self.redirect(dest)
- def POST_reg(self, *a, **kw):
+ @validate(dest = VDestination(default = "/"))
+ def POST_reg(self, dest, *a, **kw):
ApiController.POST_register(self, *a, **kw)
c.render_style = "html"
c.response_content_type = ""
- dest = request.post.get('dest', request.referer or '/')
errors = list(c.errors)
if errors:
for e in errors:
diff --git a/r2/r2/controllers/promotecontroller.py b/r2/r2/controllers/promotecontroller.py
index 31ff182cf..73539d5f6 100644
--- a/r2/r2/controllers/promotecontroller.py
+++ b/r2/r2/controllers/promotecontroller.py
@@ -168,7 +168,7 @@ class PromoteController(ListingController):
promote.reject_promo(thing, reason = reason)
# also reject anything that is live but has a reason given
elif (c.user_is_sponsor and reason and
- thing.promte_status == promote.STATUS.promoted):
+ thing.promote_status == promote.STATUS.promoted):
promote.reject_promo(thing, reason = reason)
# otherwise, mark it as "finished"
else:
@@ -220,6 +220,9 @@ class PromoteController(ListingController):
# want the URL
url = url[0].url
+ if form.has_errors('bid', errors.BAD_BID):
+ return
+
# check dates and date range
start, end = [x.date() for x in dates] if dates else (None, None)
if (not l or
@@ -242,7 +245,6 @@ class PromoteController(ListingController):
if (form.has_errors('title', errors.NO_TEXT,
errors.TOO_LONG) or
form.has_errors('url', errors.NO_URL, errors.BAD_URL) or
- form.has_errors('bid', errors.BAD_BID) or
(not l and jquery.has_errors('ratelimit', errors.RATELIMIT))):
return
elif l:
diff --git a/r2/r2/controllers/reddit_base.py b/r2/r2/controllers/reddit_base.py
index e291cbc45..a9e797acb 100644
--- a/r2/r2/controllers/reddit_base.py
+++ b/r2/r2/controllers/reddit_base.py
@@ -25,11 +25,11 @@ from pylons.controllers.util import abort, redirect_to
from pylons.i18n import _
from pylons.i18n.translation import LanguageError
from r2.lib.base import BaseController, proxyurl
-from r2.lib import pages, utils, filters
+from r2.lib import pages, utils, filters, amqp
from r2.lib.utils import http_utils, UniqueIterator
-from r2.lib.cache import LocalCache
+from r2.lib.cache import LocalCache, make_key, MemcachedError
import random as rand
-from r2.models.account import valid_cookie, FakeAccount
+from r2.models.account import valid_cookie, FakeAccount, valid_feed
from r2.models.subreddit import Subreddit
from r2.models import *
from errors import ErrorSet
@@ -37,12 +37,14 @@ from validator import *
from r2.lib.template_helpers import add_sr
from r2.lib.jsontemplates import api_type
+from Cookie import CookieError
from copy import copy
from Cookie import CookieError
from datetime import datetime
-import sha, simplejson, locale
+from hashlib import sha1, md5
from urllib import quote, unquote
-from simplejson import dumps
+import simplejson
+import locale
from r2.lib.tracking import encrypt, decrypt
@@ -224,7 +226,7 @@ def over18():
else:
if 'over18' in c.cookies:
cookie = c.cookies['over18'].value
- if cookie == sha.new(request.ip).hexdigest():
+ if cookie == sha1(request.ip).hexdigest():
return True
def set_subreddit():
@@ -281,6 +283,13 @@ def set_content_type():
return utils.to_js(content,callback = request.params.get(
"callback", "document.write"))
c.response_wrappers.append(to_js)
+ if ext in ("rss", "api", "json") and request.method.upper() == "GET":
+ user = valid_feed(request.GET.get("user"),
+ request.GET.get("feed"),
+ request.path)
+ if user:
+ c.user = user
+ c.user_is_loggedin = True
def get_browser_langs():
browser_langs = []
@@ -403,6 +412,7 @@ def ratelimit_throttled():
if throttled(ip) or throttled(subnet):
abort(503, 'service temporarily unavailable')
+
#TODO i want to get rid of this function. once the listings in front.py are
#moved into listingcontroller, we shouldn't have a need for this
#anymore
@@ -411,13 +421,16 @@ def base_listing(fn):
after = VByName('after'),
before = VByName('before'),
count = VCount('count'),
- target = VTarget("target"))
+ target = VTarget("target"),
+ show = VLength('show', 3))
def new_fn(self, before, **env):
if c.render_style == "htmllite":
c.link_target = env.get("target")
elif "target" in env:
del env["target"]
+ if "show" in env and env['show'] == 'all':
+ c.ignore_hide_rules = True
kw = build_arg_list(fn, env)
#turn before into after/reverse
@@ -429,40 +442,32 @@ def base_listing(fn):
return fn(self, **kw)
return new_fn
-class RedditController(BaseController):
+class MinimalController(BaseController):
def request_key(self):
# note that this references the cookie at request time, not
# the current value of it
- cookie_keys = []
- for x in cache_affecting_cookies:
- cookie_keys.append(request.cookies.get(x,''))
+ try:
+ cookies_key = [(x, request.cookies.get(x,''))
+ for x in cache_affecting_cookies]
+ except CookieError:
+ cookies_key = ''
- key = ''.join((str(c.lang),
- str(c.content_langs),
- request.host,
- str(c.cname),
- str(request.fullpath),
- str(c.over18),
- str(c.firsttime),
- ''.join(cookie_keys)))
- return key
+ return make_key('request_key',
+ c.lang,
+ c.content_langs,
+ request.host,
+ c.cname,
+ request.fullpath,
+ c.over18,
+ c.firsttime,
+ cookies_key)
def cached_response(self):
return c.response
- @staticmethod
- def login(user, admin = False, rem = False):
- c.cookies[g.login_cookie] = Cookie(value = user.make_cookie(admin = admin),
- expires = NEVER if rem else None)
-
- @staticmethod
- def logout(admin = False):
- c.cookies[g.login_cookie] = Cookie(value='')
-
def pre(self):
c.start_time = datetime.now(g.tz)
-
g.reset_caches()
c.domain_prefix = request.environ.get("reddit-domain-prefix",
@@ -474,10 +479,106 @@ class RedditController(BaseController):
# the domain has to be set before Cookies get initialized
set_subreddit()
+ c.errors = ErrorSet()
+ c.cookies = Cookies()
+
+ def try_pagecache(self):
+ #check content cache
+ if not c.user_is_loggedin:
+ r = g.rendercache.get(self.request_key())
+ if r and request.method == 'GET':
+ response = c.response
+ response.headers = r.headers
+ response.content = r.content
+
+ for x in r.cookies.keys():
+ if x in cache_affecting_cookies:
+ cookie = r.cookies[x]
+ response.set_cookie(key = x,
+ value = cookie.value,
+ domain = cookie.get('domain',None),
+ expires = cookie.get('expires',None),
+ path = cookie.get('path',None))
+
+ response.status_code = r.status_code
+ request.environ['pylons.routes_dict']['action'] = 'cached_response'
+ # make sure to carry over the content type
+ c.response_content_type = r.headers['content-type']
+ if r.headers.has_key('access-control'):
+ c.response_access_control = r.headers['access-control']
+ c.used_cache = True
+ # response wrappers have already been applied before cache write
+ c.response_wrappers = []
+
+
+ def post(self):
+ response = c.response
+ content = filter(None, response.content)
+ if isinstance(content, (list, tuple)):
+ content = ''.join(content)
+ for w in c.response_wrappers:
+ content = w(content)
+ response.content = content
+ if c.response_content_type:
+ response.headers['Content-Type'] = c.response_content_type
+ if c.response_access_control:
+ c.response.headers['Access-Control'] = c.response_access_control
+
+ if c.user_is_loggedin:
+ response.headers['Cache-Control'] = 'no-cache'
+ response.headers['Pragma'] = 'no-cache'
+
+ # send cookies
+ if not c.used_cache and c.cookies:
+ # if we used the cache, these cookies should be set by the
+ # cached response object instead
+ for k,v in c.cookies.iteritems():
+ if v.dirty:
+ response.set_cookie(key = k,
+ value = quote(v.value),
+ domain = v.domain,
+ expires = v.expires)
+
+ #return
+ #set content cache
+ if (g.page_cache_time
+ and request.method == 'GET'
+ and not c.user_is_loggedin
+ and not c.used_cache
+ and not c.dontcache
+ and response.status_code != 503
+ and response.content and response.content[0]):
+ try:
+ g.rendercache.set(self.request_key(),
+ response,
+ g.page_cache_time)
+ except MemcachedError:
+ # the key was too big to set in the rendercache
+ g.log.debug("Ignored too-big render cache")
+
+ if g.enable_usage_stats:
+ amqp.add_kw("usage_q",
+ start_time = c.start_time,
+ end_time = datetime.now(g.tz),
+ action = str(c.action) or "static")
+
+class RedditController(MinimalController):
+
+ @staticmethod
+ def login(user, admin = False, rem = False):
+ c.cookies[g.login_cookie] = Cookie(value = user.make_cookie(admin = admin),
+ expires = NEVER if rem else None)
+
+ @staticmethod
+ def logout(admin = False):
+ c.cookies[g.login_cookie] = Cookie(value='')
+
+ def pre(self):
+ MinimalController.pre(self)
+
set_cnameframe()
# populate c.cookies unless we're on the unsafe media_domain
- c.cookies = Cookies()
if request.host != g.media_domain or g.media_domain == g.domain:
try:
for k,v in request.cookies.iteritems():
@@ -489,7 +590,6 @@ class RedditController(BaseController):
request.environ['HTTP_COOKIE'] = ''
c.response_wrappers = []
- c.errors = ErrorSet()
c.firsttime = firsttime()
(c.user, maybe_admin) = \
valid_cookie(c.cookies[g.login_cookie].value
@@ -515,6 +615,12 @@ class RedditController(BaseController):
read_mod_cookie()
if hasattr(c.user, 'msgtime') and c.user.msgtime:
c.have_messages = c.user.msgtime
+ if hasattr(c.user, 'modmsgtime'):
+ c.show_mod_mail = True
+ if c.user.modmsgtime:
+ c.have_mod_messages = c.user.modmsgtime
+ else:
+ c.show_mod_mail = Subreddit.reverse_moderator_ids(c.user)
c.user_is_admin = maybe_admin and c.user.name in g.admins
c.user_is_sponsor = c.user_is_admin or c.user.name in g.sponsors
if not g.disallow_db_writes:
@@ -560,74 +666,6 @@ class RedditController(BaseController):
elif c.site.domain and c.site.css_on_cname and not c.cname:
c.allow_styles = False
- #check content cache
- if not c.user_is_loggedin:
- r = g.rendercache.get(self.request_key())
- if r and request.method == 'GET':
- response = c.response
- response.headers = r.headers
- response.content = r.content
-
- for x in r.cookies.keys():
- if x in cache_affecting_cookies:
- cookie = r.cookies[x]
- response.set_cookie(key = x,
- value = cookie.value,
- domain = cookie.get('domain',None),
- expires = cookie.get('expires',None),
- path = cookie.get('path',None))
-
- response.status_code = r.status_code
- request.environ['pylons.routes_dict']['action'] = 'cached_response'
- # make sure to carry over the content type
- c.response_content_type = r.headers['content-type']
- if r.headers.has_key('access-control'):
- c.response_access_control = r.headers['access-control']
- c.used_cache = True
- # response wrappers have already been applied before cache write
- c.response_wrappers = []
-
- def post(self):
- response = c.response
- content = filter(None, response.content)
- if isinstance(content, (list, tuple)):
- content = ''.join(content)
- for w in c.response_wrappers:
- content = w(content)
- response.content = content
- if c.response_content_type:
- response.headers['Content-Type'] = c.response_content_type
- if c.response_access_control:
- c.response.headers['Access-Control'] = c.response_access_control
-
- if c.user_is_loggedin:
- response.headers['Cache-Control'] = 'no-cache'
- response.headers['Pragma'] = 'no-cache'
-
- # send cookies
- if not c.used_cache and c.cookies:
- # if we used the cache, these cookies should be set by the
- # cached response object instead
- for k,v in c.cookies.iteritems():
- if v.dirty:
- response.set_cookie(key = k,
- value = quote(v.value),
- domain = v.domain,
- expires = v.expires)
-
- #return
- #set content cache
- if (g.page_cache_time
- and request.method == 'GET'
- and not c.user_is_loggedin
- and not c.used_cache
- and not c.dontcache
- and response.status_code != 503
- and response.content and response.content[0]):
- g.rendercache.set(self.request_key(),
- response,
- g.page_cache_time)
-
def check_modified(self, thing, action):
if c.user_is_loggedin:
return
@@ -644,6 +682,9 @@ class RedditController(BaseController):
def abort404(self):
abort(404, "not found")
+ def abort403(self):
+ abort(403, "forbidden")
+
def sendpng(self, string):
c.response_content_type = 'image/png'
c.response.content = string
@@ -661,7 +702,7 @@ class RedditController(BaseController):
return request.path + utils.query_string(merged)
def api_wrapper(self, kw):
- data = dumps(kw)
+ data = simplejson.dumps(kw)
if request.method == "GET" and request.GET.get("callback"):
return "%s(%s)" % (websafe_json(request.GET.get("callback")),
websafe_json(data))
diff --git a/r2/r2/controllers/usage.py b/r2/r2/controllers/usage.py
new file mode 100644
index 000000000..c2d54326f
--- /dev/null
+++ b/r2/r2/controllers/usage.py
@@ -0,0 +1,34 @@
+# 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
+# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
+# License Version 1.1, but Sections 14 and 15 have been added to cover use of
+# 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-2010
+# CondeNet, Inc. All Rights Reserved.
+################################################################################
+from pylons import request, g
+from reddit_base import RedditController
+from r2.lib.pages import AdminPage, AdminUsage
+from validator import *
+
+class UsageController(RedditController):
+
+ @validate(VAdmin())
+ def GET_index(self):
+ res = AdminPage(content = AdminUsage(),
+ show_sidebar = False,
+ title = 'usage').render()
+ return res
diff --git a/r2/r2/controllers/validator/validator.py b/r2/r2/controllers/validator/validator.py
index 519572bc1..7efb4be8b 100644
--- a/r2/r2/controllers/validator/validator.py
+++ b/r2/r2/controllers/validator/validator.py
@@ -24,11 +24,12 @@ from pylons.i18n import _
from pylons.controllers.util import abort
from r2.lib import utils, captcha, promote
from r2.lib.filters import unkeep_space, websafe, _force_unicode
+from r2.lib.filters import markdown_souptest
from r2.lib.db.operators import asc, desc
from r2.lib.template_helpers import add_sr
from r2.lib.jsonresponse import json_respond, JQueryResponse, JsonResponse
from r2.lib.jsontemplates import api_type
-
+from r2.lib.log import log_text
from r2.models import *
from r2.lib.authorize import Address, CreditCard
@@ -37,6 +38,7 @@ from r2.controllers.errors import VerifiedUserRequiredException
from copy import copy
from datetime import datetime, timedelta
+from curses.ascii import isprint
import re, inspect
import pycountry
@@ -123,7 +125,6 @@ def _make_validated_kw(fn, simple_vals, param_vals, env):
for var, validator in param_vals.iteritems():
kw[var] = validator(env)
return kw
-
def validate(*simple_vals, **param_vals):
def val(fn):
@@ -230,7 +231,7 @@ class VRequired(Validator):
if not e: e = self._error
if e:
self.set_error(e)
-
+
def run(self, item):
if not item:
self.error()
@@ -266,6 +267,25 @@ class VCommentByID(VThing):
def __init__(self, param, redirect = True, *a, **kw):
VThing.__init__(self, param, Comment, redirect=redirect, *a, **kw)
+class VAd(VThing):
+ def __init__(self, param, redirect = True, *a, **kw):
+ VThing.__init__(self, param, Ad, redirect=redirect, *a, **kw)
+
+class VAdByCodename(Validator):
+ def run(self, codename, required_fullname=None):
+ if not codename:
+ return self.set_error(errors.NO_TEXT)
+
+ try:
+ a = Ad._by_codename(codename)
+ except NotFound:
+ a = None
+
+ if a and required_fullname and a._fullname != required_fullname:
+ return self.set_error(errors.INVALID_OPTION)
+ else:
+ return a
+
class VAward(VThing):
def __init__(self, param, redirect = True, *a, **kw):
VThing.__init__(self, param, Award, redirect=redirect, *a, **kw)
@@ -314,7 +334,7 @@ class VMessageID(Validator):
try:
cid = int(cid, 36)
m = Message._byID(cid, True)
- if not m.can_view():
+ if not m.can_view_slow():
abort(403, 'forbidden')
return m
except (NotFound, ValueError):
@@ -324,14 +344,23 @@ class VCount(Validator):
def run(self, count):
if count is None:
count = 0
- return max(int(count), 0)
+ try:
+ return max(int(count), 0)
+ except ValueError:
+ return 0
class VLimit(Validator):
def run(self, limit):
if limit is None:
- return c.user.pref_numsites
- return min(max(int(limit), 1), 100)
+ return c.user.pref_numsites
+
+ try:
+ i = int(limit)
+ except ValueError:
+ return c.user.pref_numsites
+
+ return min(max(i, 1), 100)
class VCssMeasure(Validator):
measure = re.compile(r"^\s*[\d\.]+\w{0,3}\s*$")
@@ -371,23 +400,47 @@ class VLength(Validator):
self.set_error(self.length_error, {'max_length': self.max_length})
else:
return text
-
+
+class VPrintable(VLength):
+ def run(self, text, text2 = ''):
+ text = VLength.run(self, text, text2)
+
+ if text is None:
+ return None
+
+ try:
+ if all(isprint(str(x)) for x in text):
+ return str(text)
+ except UnicodeEncodeError:
+ pass
+
+ self.set_error(errors.BAD_STRING)
+ return None
+
+
class VTitle(VLength):
def __init__(self, param, max_length = 300, **kw):
VLength.__init__(self, param, max_length, **kw)
-
-class VComment(VLength):
- def __init__(self, param, max_length = 10000, **kw):
- VLength.__init__(self, param, max_length, **kw)
-
-class VSelfText(VLength):
- def __init__(self, param, max_length = 10000, **kw):
- VLength.__init__(self, param, max_length, **kw)
-
-class VMessage(VLength):
+
+class VMarkdown(VLength):
def __init__(self, param, max_length = 10000, **kw):
VLength.__init__(self, param, max_length, **kw)
+ def run(self, text, text2 = ''):
+ text = text or text2
+ VLength.run(self, text)
+ try:
+ markdown_souptest(text)
+ return text
+ except ValueError:
+ import sys
+ user = "???"
+ if c.user_is_loggedin:
+ user = c.user.name
+ g.log.error("HAX by %s: %s" % (user, text))
+ s = sys.exc_info()
+ # reraise the original error with the original stack trace
+ raise s[1], None, s[2]
class VSubredditName(VRequired):
def __init__(self, item, *a, **kw):
@@ -422,7 +475,7 @@ class VSubredditDesc(Validator):
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:
@@ -486,7 +539,7 @@ class VUser(Validator):
if (password is not None) and not valid_password(c.user, password):
self.set_error(errors.WRONG_PASSWORD)
-
+
class VModhash(Validator):
default_param = 'uh'
def run(self, uh):
@@ -595,7 +648,10 @@ class VSubmitParent(VByName):
if fullname:
parent = VByName.run(self, fullname)
if parent and parent._deleted:
- self.set_error(errors.DELETED_COMMENT)
+ if isinstance(parent, Link):
+ self.set_error(errors.DELETED_LINK)
+ else:
+ self.set_error(errors.DELETED_COMMENT)
if isinstance(parent, Message):
return parent
else:
@@ -623,7 +679,7 @@ class VSubmitSR(Validator):
self.set_error(errors.SUBREDDIT_NOTALLOWED)
else:
return sr
-
+
pass_rx = re.compile(r"^.{3,20}$")
def chkpass(x):
@@ -633,12 +689,10 @@ class VPassword(Validator):
def run(self, password, verify):
if not chkpass(password):
self.set_error(errors.BAD_PASSWORD)
- return
elif verify != password:
self.set_error(errors.BAD_PASSWORD_MATCH)
- return password
else:
- return password
+ return password.encode('utf8')
user_rx = re.compile(r"^[\w-]{3,20}$", re.UNICODE)
@@ -667,11 +721,15 @@ class VUname(VRequired):
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
if user_name:
+ try:
+ str(password)
+ except UnicodeEncodeError:
+ password = password.encode('utf8')
user = valid_login(user_name, password)
if not user:
return self.error()
@@ -698,7 +756,7 @@ class VUrl(VRequired):
sr = None
else:
sr = None
-
+
if not url:
return self.error(errors.NO_URL)
url = utils.sanitize_url(url)
@@ -736,6 +794,21 @@ class VExistingUname(VRequired):
return self.error(errors.USER_DOESNT_EXIST)
self.error()
+class VMessageRecipent(VExistingUname):
+ def run(self, name):
+ if not name:
+ return self.error()
+ if name.startswith('#'):
+ try:
+ s = Subreddit._by_name(name.strip('#'))
+ if isinstance(s, FakeSubreddit):
+ raise NotFound, "fake subreddit"
+ return s
+ except NotFound:
+ self.set_error(errors.SUBREDDIT_NOEXIST)
+ else:
+ return VExistingUname.run(self, name)
+
class VUserWithEmail(VExistingUname):
def run(self, name):
user = VExistingUname.run(self, name)
@@ -901,9 +974,11 @@ class VRatelimit(Validator):
class VCommentIDs(Validator):
#id_str is a comma separated list of id36's
def run(self, id_str):
- cids = [int(i, 36) for i in id_str.split(',')]
- comments = Comment._byID(cids, data=True, return_dict = False)
- return comments
+ if id_str:
+ cids = [int(i, 36) for i in id_str.split(',')]
+ comments = Comment._byID(cids, data=True, return_dict = False)
+ return comments
+ return []
class CachedUser(object):
@@ -1049,14 +1124,9 @@ class ValidIP(Validator):
self.set_error(errors.BANNED_IP)
return request.ip
-class ValidDomain(Validator):
+class VOkayDomain(Validator):
def run(self, url):
- if url and is_banned_domain(url):
- self.set_error(errors.BANNED_DOMAIN)
-
-
-
-
+ return is_banned_domain(url)
class VDate(Validator):
"""
@@ -1135,9 +1205,32 @@ class VDestination(Validator):
def __init__(self, param = 'dest', default = "", **kw):
self.default = default
Validator.__init__(self, param, **kw)
-
+
def run(self, dest):
- return dest or request.referer or self.default
+ if not dest:
+ dest = request.referer or self.default or "/"
+
+ ld = dest.lower()
+ if (ld.startswith("/") or
+ ld.startswith("http://") or
+ ld.startswith("https://")):
+
+ u = UrlParser(dest)
+
+ if u.is_reddit_url():
+ return dest
+
+ ip = getattr(request, "ip", "[unknown]")
+ fp = getattr(request, "fullpath", "[unknown]")
+ dm = c.domain or "[unknown]"
+ cn = c.cname or "[unknown]"
+
+ log_text("invalid redirect",
+ "%s attempted to redirect from %s to %s with domain %s and cname %s"
+ % (ip, fp, dest, dm, cn),
+ "info")
+
+ return "/"
class ValidAddress(Validator):
def __init__(self, param, usa_only = True):
diff --git a/r2/r2/i18n/r2.pot b/r2/r2/i18n/r2.pot
index 22e92c92c..8cbd4a200 100644
--- a/r2/r2/i18n/r2.pot
+++ b/r2/r2/i18n/r2.pot
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: r2 0.0.0\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2010-01-25 14:44-0700\n"
+"POT-Creation-Date: 2010-02-06 23:44-0700\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME ", "
+tags from the output.
+.TP
+.B MKD_NOLINKS
+Do not process
+.B []
+and remove
+.B
+tags from the output.
+.TP
+.B MKD_NOPANTS
+Suppress Smartypants-style replacement of quotes, dashes, or ellipses.
+.TP
+.B MKD_STRICT
+Disable superscript and relaxed emphasis processing if configured; otherwise a no-op.
+.TP
+.B MKD_TAGTEXT
+Process as inside an
+.SM HTML
+tag: no
+.BR ,
+no
+.BR
", DENY_IMG|INSIDE_TAG, IS_URL };
+static linkytype linkt = { 0, 0, "", "", DENY_A, IS_URL };
+
+/*
+ * pseudo-protocols for [][];
+ *
+ * id: generates tag
+ * class: generates tag
+ * raw: just dump the link without any processing
+ */
+static linkytype specials[] = {
+ { "id:", 3, "", "", 0, IS_URL },
+ { "class:", 6, "", "", 0, 0 },
+ { "raw:", 4, 0, 0, 0, 0, 0, DENY_HTML, 0 },
+ { "abbr:", 5, "", "", 0, 0 },
+} ;
+
+#define NR(x) (sizeof x / sizeof x[0])
+
+/* see if t contains one of our pseudo-protocols.
+ */
+static linkytype *
+pseudo(Cstring t)
+{
+ int i;
+ linkytype *r;
+
+ for ( i=0; i < NR(specials); i++ ) {
+ r = &specials[i];
+ if ( (S(t) > r->szpat) && (strncasecmp(T(t), r->pat, r->szpat) == 0) )
+ return r;
+ }
+ return 0;
+}
+
+
+/* print out a linky (or fail if it's Not Allowed)
+ */
+static int
+linkyformat(MMIOT *f, Cstring text, int image, Footnote *ref)
+{
+ linkytype *tag;
+ char *edit;
+
+ if ( image )
+ tag = &imaget;
+ else if ( tag = pseudo(ref->link) ) {
+ if ( f->flags & (NO_PSEUDO_PROTO|SAFELINK) )
+ return 0;
+ }
+ else if ( (f->flags & SAFELINK) && T(ref->link)
+ && (T(ref->link)[0] != '/')
+ && !isautoprefix(T(ref->link)) )
+ /* if SAFELINK, only accept links that are local or
+ * a well-known protocol
+ */
+ return 0;
+ else
+ tag = &linkt;
+
+ if ( f->flags & tag->flags )
+ return 0;
+
+ if ( tag->link_pfx ) {
+ Qstring(tag->link_pfx, f);
+
+ if ( tag->kind & IS_URL ) {
+ if ( f->e_url && (edit = (*f->e_url)(T(ref->link), S(ref->link), f->e_context)) ) {
+ puturl(edit, strlen(edit), f, 0);
+ if ( f->e_free ) (*f->e_free)(edit, f->e_context);
+ }
+ else {
+ if ( f->base && T(ref->link) && (T(ref->link)[tag->szpat] == '/') )
+ puturl(f->base, strlen(f->base), f, 0);
+ puturl(T(ref->link) + tag->szpat, S(ref->link) - tag->szpat, f, 0);
+ }
+ }
+ else
+ ___mkd_reparse(T(ref->link) + tag->szpat, S(ref->link) - tag->szpat, INSIDE_TAG, f);
+
+ Qstring(tag->link_sfx, f);
+
+ if ( f->e_flags && (edit = (*f->e_flags)(T(ref->link), S(ref->link), f->e_context)) ) {
+ Qchar(' ', f);
+ Qstring(edit, f);
+ if ( f->e_free ) (*f->e_free)(edit, f->e_context);
+ }
+
+ if ( tag->WxH) {
+ if ( ref->height) Qprintf(f," height=\"%d\"", ref->height);
+ if ( ref->width) Qprintf(f, " width=\"%d\"", ref->width);
+ }
+
+ if ( S(ref->title) ) {
+ Qstring(" title=\"", f);
+ ___mkd_reparse(T(ref->title), S(ref->title), INSIDE_TAG, f);
+ Qchar('"', f);
+ }
+
+ Qstring(tag->text_pfx, f);
+ ___mkd_reparse(T(text), S(text), tag->flags, f);
+ Qstring(tag->text_sfx, f);
+ }
+ else
+ Qwrite(T(ref->link) + tag->szpat, S(ref->link) - tag->szpat, f);
+
+ return 1;
+} /* linkyformat */
+
+
+/*
+ * process embedded links and images
+ */
+static int
+linkylinky(int image, MMIOT *f)
+{
+ int start = mmiottell(f);
+ Cstring name;
+ Footnote key, *ref;
+
+ int status = 0;
+
+ CREATE(name);
+ memset(&key, 0, sizeof key);
+
+ if ( linkylabel(f, &name) ) {
+ if ( peek(f,1) == '(' ) {
+ pull(f);
+ if ( linkyurl(f, image, &key) )
+ status = linkyformat(f, name, image, &key);
+ }
+ else {
+ int goodlink, implicit_mark = mmiottell(f);
+
+ if ( eatspace(f) == '[' ) {
+ pull(f); /* consume leading '[' */
+ goodlink = linkylabel(f, &key.tag);
+ }
+ else {
+ /* new markdown implicit name syntax doesn't
+ * require a second []
+ */
+ mmiotseek(f, implicit_mark);
+ goodlink = !(f->flags & MKD_1_COMPAT);
+ }
+
+ if ( goodlink ) {
+ if ( !S(key.tag) ) {
+ DELETE(key.tag);
+ T(key.tag) = T(name);
+ S(key.tag) = S(name);
+ }
+
+ if ( ref = bsearch(&key, T(*f->footnotes), S(*f->footnotes),
+ sizeof key, (stfu)__mkd_footsort) )
+ status = linkyformat(f, name, image, ref);
+ }
+ }
+ }
+
+ DELETE(name);
+ ___mkd_freefootnote(&key);
+
+ if ( status == 0 )
+ mmiotseek(f, start);
+
+ return status;
+}
+
+
+/* write a character to output, doing text escapes ( & -> &,
+ * > -> > < -> < )
+ */
+static void
+cputc(int c, MMIOT *f)
+{
+ switch (c) {
+ case '&': Qstring("&", f); break;
+ case '>': Qstring(">", f); break;
+ case '<': Qstring("<", f); break;
+ default : Qchar(c, f); break;
+ }
+}
+
+
+/*
+ * convert an email address to a string of nonsense
+ */
+static void
+mangle(char *s, int len, MMIOT *f)
+{
+ while ( len-- > 0 ) {
+ Qstring("", f);
+ Qprintf(f, COINTOSS() ? "x%02x;" : "%02d;", *((unsigned char*)(s++)) );
+ }
+}
+
+
+/* before letting a tag through, validate against
+ * DENY_A and DENY_IMG
+ */
+static int
+forbidden_tag(MMIOT *f)
+{
+ int c = toupper(peek(f, 1));
+
+ if ( f->flags & DENY_HTML )
+ return 1;
+
+ if ( c == 'A' && (f->flags & DENY_A) && !isthisalnum(f,2) )
+ return 1;
+ if ( c == 'I' && (f->flags & DENY_IMG)
+ && strncasecmp(cursor(f)+1, "MG", 2) == 0
+ && !isthisalnum(f,4) )
+ return 1;
+ return 0;
+}
+
+
+/* Check a string to see if it looks like a mail address
+ * "looks like a mail address" means alphanumeric + some
+ * specials, then a `@`, then alphanumeric + some specials,
+ * but with a `.`
+ */
+static int
+maybe_address(char *p, int size)
+{
+ int ok = 0;
+
+ for ( ;size && (isalnum(*p) || strchr("._-+*", *p)); ++p, --size)
+ ;
+
+ if ( ! (size && *p == '@') )
+ return 0;
+
+ --size, ++p;
+
+ if ( size && *p == '.' ) return 0;
+
+ for ( ;size && (isalnum(*p) || strchr("._-+", *p)); ++p, --size )
+ if ( *p == '.' && size > 1 ) ok = 1;
+
+ return size ? 0 : ok;
+}
+
+
+/* The size-length token at cursor(f) is either a mailto:, an
+ * implicit mailto:, one of the approved url protocols, or just
+ * plain old text. If it's a mailto: or an approved protocol,
+ * linkify it, otherwise say "no"
+ */
+static int
+process_possible_link(MMIOT *f, int size)
+{
+ int address= 0;
+ int mailto = 0;
+ char *text = cursor(f);
+
+ if ( f->flags & DENY_A ) return 0;
+
+ if ( (size > 7) && strncasecmp(text, "mailto:", 7) == 0 ) {
+ /* if it says it's a mailto, it's a mailto -- who am
+ * I to second-guess the user?
+ */
+ address = 1;
+ mailto = 7; /* 7 is the length of "mailto:"; we need this */
+ }
+ else
+ address = maybe_address(text, size);
+
+ if ( address ) {
+ Qstring("", f);
+ mangle(text+mailto, size-mailto, f);
+ Qstring("", f);
+ return 1;
+ }
+ else if ( isautoprefix(text) ) {
+ char *edit;
+ Qstring("e_url && (edit = (*f->e_url)(text, size, f->e_context)) ) {
+ puturl(edit, strlen(edit), f, 0);
+ if ( f->e_free ) (*f->e_free)(edit, f->e_context);
+ }
+ else
+ puturl(text,size,f, 0);
+ if ( f->e_flags && (edit = (*f->e_flags)(text, size, f->e_context)) ) {
+ Qstring("\" ", f);
+ Qstring(edit, f);
+ if ( f->e_free ) (*f->e_free)(edit, f->e_context);
+ Qchar('>', f);
+ }
+ else
+ Qstring("\">", f);
+ puturl(text,size,f, 1);
+ Qstring("", f);
+ return 1;
+ }
+ return 0;
+} /* process_possible_link */
+
+
+/* a < may be just a regular character, the start of an embedded html
+ * tag, or the start of an
", f);
+ break;
+
+ case '>': if ( tag_text(f) )
+ Qstring(">", f);
+ else
+ Qchar(c, f);
+ break;
+
+ case '"': if ( tag_text(f) )
+ Qstring(""", f);
+ else
+ Qchar(c, f);
+ break;
+
+ case '!': if ( peek(f,1) == '[' ) {
+ pull(f);
+ if ( tag_text(f) || !linkylinky(1, f) )
+ Qstring("![", f);
+ }
+ else
+ Qchar(c, f);
+ break;
+ case '[': if ( tag_text(f) || !linkylinky(0, f) )
+ Qchar(c, f);
+ break;
+#if SUPERSCRIPT
+ /* A^B -> AB */
+ case '^': if ( (f->flags & (STRICT|INSIDE_TAG)) || isthisspace(f,-1) || isthisspace(f,1) )
+ Qchar(c,f);
+ else {
+ char *sup = cursor(f);
+ int len = 0;
+ Qstring("",f);
+ while ( !isthisspace(f,1+len) ) {
+ ++len;
+ }
+ shift(f,len);
+ ___mkd_reparse(sup, len, 0, f);
+ Qstring("", f);
+ }
+ break;
+#endif
+ case '_':
+#if RELAXED_EMPHASIS
+ /* Underscores don't count if they're in the middle of a word */
+ if ( !(f->flags & STRICT) && isthisalnum(f,-1)
+ && isthisalnum(f,1) ) {
+ Qchar(c, f);
+ break;
+ }
+#endif
+ case '*':
+#if RELAXED_EMPHASIS
+ /* Underscores & stars don't count if they're out in the middle
+ * of whitespace */
+ if ( !(f->flags & STRICT) && isthisspace(f,-1)
+ && isthisspace(f,1) ) {
+ Qchar(c, f);
+ break;
+ }
+ /* else fall into the regular old emphasis case */
+#endif
+ if ( tag_text(f) )
+ Qchar(c, f);
+ else {
+ for (rep = 1; peek(f,1) == c; pull(f) )
+ ++rep;
+ Qem(f,c,rep);
+ }
+ break;
+
+ case '`': if ( tag_text(f) || !iscodeblock(f) )
+ Qchar(c, f);
+ else {
+ Qstring("", f);
+ if ( peek(f, 1) == '`' ) {
+ pull(f);
+ code(2, f);
+ }
+ else
+ code(1, f);
+ Qstring("", f);
+ }
+ break;
+
+ case '\\': switch ( c = pull(f) ) {
+ case '&': Qstring("&", f);
+ break;
+ case '<': Qstring("<", f);
+ break;
+ case '>': case '#': case '.': case '-':
+ case '+': case '{': case '}': case ']':
+ case '!': case '[': case '*': case '_':
+ case '\\':case '(': case ')':
+ case '`': Qchar(c, f);
+ break;
+ default:
+ Qchar('\\', f);
+ if ( c != EOF )
+ shift(f,-1);
+ break;
+ }
+ break;
+
+ case '<': if ( !maybe_tag_or_link(f) )
+ Qstring("<", f);
+ break;
+
+ case '&': j = (peek(f,1) == '#' ) ? 2 : 1;
+ while ( isthisalnum(f,j) )
+ ++j;
+
+ if ( peek(f,j) != ';' )
+ Qstring("&", f);
+ else
+ Qchar(c, f);
+ break;
+
+ default: Qchar(c, f);
+ break;
+ }
+ }
+ /* truncate the input string after we've finished processing it */
+ S(f->in) = f->isp = 0;
+} /* text */
+
+
+static int
+iscodeblock(MMIOT *f)
+{
+ int i=1, single = 1, c;
+
+ if ( peek(f,i) == '`' ) {
+ single=0;
+ i++;
+ }
+ while ( (c=peek(f,i)) != EOF ) {
+ if ( (c == '`') && (single || peek(f,i+1) == '`') )
+ return 1;
+ else if ( c == '\\' )
+ i++;
+ i++;
+ }
+ return 0;
+
+}
+
+static int
+endofcode(int escape, int offset, MMIOT *f)
+{
+ switch (escape) {
+ case 2: if ( peek(f, offset+1) == '`' ) {
+ shift(f,1);
+ case 1: shift(f,offset);
+ return 1;
+ }
+ default:return 0;
+ }
+}
+
+
+/* the only characters that have special meaning in a code block are
+ * `<' and `&' , which are /always/ expanded to < and &
+ */
+static void
+code(int escape, MMIOT *f)
+{
+ int c;
+
+ if ( escape && (peek(f,1) == ' ') )
+ shift(f,1);
+
+ while ( (c = pull(f)) != EOF ) {
+ switch (c) {
+ case ' ': if ( peek(f,1) == '`' && endofcode(escape, 1, f) )
+ return;
+ Qchar(c, f);
+ break;
+
+ case '`': if ( endofcode(escape, 0, f) )
+ return;
+ Qchar(c, f);
+ break;
+
+ case '\\': cputc(c, f);
+ if ( peek(f,1) == '>' || (c = pull(f)) == EOF )
+ break;
+
+ case 003: /* ^C; expand back to spaces */
+ Qstring(" ", f);
+ break;
+
+ default: cputc(c, f);
+ break;
+ }
+ }
+} /* code */
+
+
+/* print a header block
+ */
+static void
+printheader(Paragraph *pp, MMIOT *f)
+{
+ Qprintf(f, "\n", f);
+ while ( idx < S(p->text) ) {
+ first = idx;
+ if ( force && (colno >= S(align)-1) )
+ idx = S(p->text);
+ else
+ while ( (idx < S(p->text)) && (T(p->text)[idx] != '|') )
+ ++idx;
+
+ Qprintf(f, "<%s%s>",
+ block,
+ alignments[ (colno < S(align)) ? T(align)[colno] : a_NONE ]);
+ ___mkd_reparse(T(p->text)+first, idx-first, 0, f);
+ Qprintf(f, "%s>\n", block);
+ idx++;
+ colno++;
+ }
+ if ( force )
+ while (colno < S(align) ) {
+ Qprintf(f, "<%s>%s>\n", block, block);
+ ++colno;
+ }
+ Qstring(" \n", f);
+ return colno;
+}
+
+static int
+printtable(Paragraph *pp, MMIOT *f)
+{
+ /* header, dashes, then lines of content */
+
+ Line *hdr, *dash, *body;
+ Istring align;
+ int start;
+ int hcols;
+ char *p;
+
+ if ( !(pp->text && pp->text->next) )
+ return 0;
+
+ hdr = pp->text;
+ dash= hdr->next;
+ body= dash->next;
+
+ /* first figure out cell alignments */
+
+ CREATE(align);
+
+ for (p=T(dash->text), start=0; start < S(dash->text); ) {
+ char first, last;
+ int end;
+
+ last=first=0;
+ for (end=start ; (end < S(dash->text)) && p[end] != '|'; ++ end ) {
+ if ( !isspace(p[end]) ) {
+ if ( !first) first = p[end];
+ last = p[end];
+ }
+ }
+ EXPAND(align) = ( first == ':' ) ? (( last == ':') ? a_CENTER : a_LEFT)
+ : (( last == ':') ? a_RIGHT : a_NONE );
+ start = 1+end;
+ }
+
+ Qstring("\n", f);
+ Qstring("\n", f);
+ hcols = splat(hdr, "th", align, 0, f);
+ Qstring("\n", f);
+
+ if ( hcols < S(align) )
+ S(align) = hcols;
+ else
+ while ( hcols > S(align) )
+ EXPAND(align) = a_NONE;
+
+ Qstring("\n", f);
+ for ( ; body; body = body->next)
+ splat(body, "td", align, 1, f);
+ Qstring("\n", f);
+ Qstring("
\n", f);
+
+ DELETE(align);
+ return 1;
+}
+
+
+static int
+printblock(Paragraph *pp, MMIOT *f)
+{
+ Line *t = pp->text;
+ static char *Begin[] = { "", "
", f);
+ code(0, f);
+ Qstring("", f);
+}
+
+
+static void
+printhtml(Line *t, MMIOT *f)
+{
+ int blanks;
+
+ for ( blanks=0; t ; t = t->next )
+ if ( S(t->text) ) {
+ for ( ; blanks; --blanks )
+ Qchar('\n', f);
+
+ Qwrite(T(t->text), S(t->text), f);
+ Qchar('\n', f);
+ }
+ else
+ blanks++;
+}
+
+
+static void
+htmlify(Paragraph *p, char *block, char *arguments, MMIOT *f)
+{
+ ___mkd_emblock(f);
+ if ( block )
+ Qprintf(f, arguments ? "<%s %s>" : "<%s>", block, arguments);
+ ___mkd_emblock(f);
+
+ while (( p = display(p, f) )) {
+ ___mkd_emblock(f);
+ Qstring("\n\n", f);
+ }
+
+ if ( block )
+ Qprintf(f, "%s>", block);
+ ___mkd_emblock(f);
+}
+
+
+#if DL_TAG_EXTENSION
+static void
+definitionlist(Paragraph *p, MMIOT *f)
+{
+ Line *tag;
+
+ if ( p ) {
+ Qstring("