diff --git a/r2/r2/controllers/api.py b/r2/r2/controllers/api.py index e7335ec0e..edad3bd9b 100644 --- a/r2/r2/controllers/api.py +++ b/r2/r2/controllers/api.py @@ -29,7 +29,7 @@ from validator import * from r2.models import * import r2.models.thing_changes as tc -from r2.lib.utils import get_title, sanitize_url, timeuntil +from r2.lib.utils import get_title, sanitize_url, timeuntil, set_last_modified from r2.lib.wrapped import Wrapped from r2.lib.pages import FriendList, ContributorList, ModList, \ BannedList, BoringPage, FormPage, NewLink @@ -278,6 +278,10 @@ class ApiController(RedditController): if should_ratelimit: VRatelimit.ratelimit(rate_user=True, rate_ip = True) + #update the modified flags + set_last_modified(c.user, 'overview') + set_last_modified(c.user, 'submitted') + # flag search indexer that something has changed tc.changed(l) @@ -598,6 +602,12 @@ class ApiController(RedditController): # flag search indexer that something has changed tc.changed(item) + #update last modified + set_last_modified(c.user, 'overview') + set_last_modified(c.user, 'commented') + set_last_modified(link, 'comments') + + #set the ratelimiter if should_ratelimit: VRatelimit.ratelimit(rate_user=True, rate_ip = True, prefix = "rate_comment_") @@ -623,6 +633,10 @@ class ApiController(RedditController): organic = vote_type == 'organic' Vote.vote(user, thing, dir, ip, spam, organic) + #update last modified + set_last_modified(c.user, 'liked') + set_last_modified(c.user, 'disliked') + # flag search indexer that something has changed tc.changed(thing) diff --git a/r2/r2/controllers/error.py b/r2/r2/controllers/error.py index 5cc74cb33..3e49a2c83 100644 --- a/r2/r2/controllers/error.py +++ b/r2/r2/controllers/error.py @@ -128,21 +128,18 @@ class ErrorController(RedditController): content=pages.UnfoundPage(choice=ch)) return res.render() - def send503(self): - c.response.status_code = 503 - c.response.headers['Retry-After'] = 1 - c.response.content = toofast - return c.response - def GET_document(self): try: code = request.GET.get('code', '') - if code == '500': + if code == '403': + return self.send403() + elif code == '500': return redditbroke % rand_strings.sadmessages elif code == '503': - return self.send503() - elif code == '403': - return self.send403() + c.response.status_code = 503 + c.response.headers['Retry-After'] = 1 + c.response.content = toofast + return c.response elif c.site: return self.send404() else: diff --git a/r2/r2/controllers/front.py b/r2/r2/controllers/front.py index 616579328..3f9835cbf 100644 --- a/r2/r2/controllers/front.py +++ b/r2/r2/controllers/front.py @@ -97,6 +97,9 @@ class FrontController(RedditController): if not c.default_sr and c.site._id != article.sr_id: return self.abort404() + #check for 304 + self.check_modified(article, 'comments') + # if there is a focal comment, communicate down to comment_skeleton.html who # that will be if comment: @@ -185,6 +188,7 @@ class FrontController(RedditController): # overview page is a merge of comments and links if location == 'overview': + self.check_modified(vuser, 'overview') links = Link._query(Link.c.author_id == vuser._id, Link.c._spam == (True, False)) comments = Comment._query(Comment.c.author_id == vuser._id, @@ -192,11 +196,13 @@ class FrontController(RedditController): query = thing.Merge((links, comments), sort = db_sort, data = True) elif location == 'comments': + self.check_modified(vuser, 'commented') query = Comment._query(Comment.c.author_id == vuser._id, Comment.c._spam == (True, False), sort = db_sort) elif location == 'submitted': + self.check_modified(vuser, 'submitted') query = Link._query(Link.c.author_id == vuser._id, Link.c._spam == (True, False), sort = db_sort) @@ -204,6 +210,7 @@ class FrontController(RedditController): # (dis)liked page: pull votes and extract thing2 elif ((location == 'liked' or location == 'disliked') and votes_visible(vuser)): + self.check_modified(vuser, location) rel = Vote.rel(vuser, Link) query = rel._query(rel.c._thing1_id == vuser._id, rel.c._t2_deleted == False) diff --git a/r2/r2/controllers/reddit_base.py b/r2/r2/controllers/reddit_base.py index a4dd53d6c..20d7b4e24 100644 --- a/r2/r2/controllers/reddit_base.py +++ b/r2/r2/controllers/reddit_base.py @@ -392,7 +392,6 @@ class RedditController(BaseController): c.used_cache = True # response wrappers have already been applied before cache write c.response_wrappers = [] - def post(self): response = c.response @@ -421,6 +420,16 @@ class RedditController(BaseController): config.cache.set(self.request_key(), response, g.page_cache_time) + + def check_modified(self, thing, action): + if c.user_is_loggedin: + return + + date = utils.is_modified_since(thing, action, request.if_modified_since) + if date is True: + abort(304, 'not modified') + else: + c.response.headers['Last-Modified'] = utils.http_date_str(date) def abort404(self): abort(404, 'not found') diff --git a/r2/r2/lib/base.py b/r2/r2/lib/base.py index f4fa39dd6..a243bdd70 100644 --- a/r2/r2/lib/base.py +++ b/r2/r2/lib/base.py @@ -26,7 +26,7 @@ from pylons.i18n import N_, _, ungettext, get_lang import r2.lib.helpers as h from r2.lib.utils import to_js from r2.lib.filters import spaceCompress -from utils import storify, string2js +from utils import storify, string2js, read_http_date import re, md5 from urllib import quote @@ -65,6 +65,12 @@ class BaseController(WSGIController): request.path = environ.get('PATH_INFO') request.user_agent = environ.get('HTTP_USER_AGENT') request.fullpath = environ.get('FULLPATH', request.path) + + if_modified_since = environ.get('HTTP_IF_MODIFIED_SINCE') + if if_modified_since: + request.if_modified_since = read_http_date(if_modified_since) + else: + request.if_modified_since = None #set the function to be called action = request.environ['pylons.routes_dict'].get('action') diff --git a/r2/r2/lib/utils/__init__.py b/r2/r2/lib/utils/__init__.py index 15bb3a57e..ddf6594af 100644 --- a/r2/r2/lib/utils/__init__.py +++ b/r2/r2/lib/utils/__init__.py @@ -20,6 +20,8 @@ # CondeNet, Inc. All Rights Reserved. ################################################################################ from utils import * +from http_utils import * +from thing_utils import * try: from r2admin.lib.admin_utils import * diff --git a/r2/r2/lib/utils/http_utils.py b/r2/r2/lib/utils/http_utils.py new file mode 100644 index 000000000..5613fa488 --- /dev/null +++ b/r2/r2/lib/utils/http_utils.py @@ -0,0 +1,21 @@ +import pytz +from datetime import datetime + +DATE_RFC822 = '%a, %d %b %Y %H:%M:%S %Z' +DATE_RFC850 = '%A, %d-%b-%y %H:%M:%S %Z' +DATE_ANSI = '%a %b %d %H:%M:%S %Y' + +def read_http_date(date_str): + try: + date = datetime.strptime(date_str, DATE_RFC822) + except ValueError: + try: + date = datetime.strptime(date_str, DATE_RFC850) + except ValueError: + date = datetime.strptime(date_str, DATE_ANSI) + date = date.replace(tzinfo = pytz.timezone('GMT')) + return date + +def http_date_str(date): + date = date.astimezone(pytz.timezone('GMT')) + return date.strftime(DATE_RFC822) diff --git a/r2/r2/lib/utils/thing_utils.py b/r2/r2/lib/utils/thing_utils.py new file mode 100644 index 000000000..081846242 --- /dev/null +++ b/r2/r2/lib/utils/thing_utils.py @@ -0,0 +1,32 @@ +from datetime import datetime +import pytz + +def make_last_modified(): + last_modified = datetime.now(pytz.timezone('GMT')) + last_modified = last_modified.replace(microsecond = 0) + return last_modified + +def is_modified_since(thing, action, date): + """Returns true if the date is older than the last_[action] date, + which means a 304 should be returned. Otherwise returns the date + that should be sent as the last-modified header.""" + from pylons import g + + prop = 'last_' + action + if not hasattr(thing, prop): + last_modified = make_last_modified() + setattr(thing, prop, last_modified) + thing._commit() + else: + last_modified = getattr(thing, prop) + + if not date or date < last_modified: + return last_modified + + #if a date was passed in and it's equal to last modified + return True + +def set_last_modified(thing, action): + from pylons import g + setattr(thing, 'last_' + action, make_last_modified()) + thing._commit() diff --git a/r2/r2/lib/utils/utils.py b/r2/r2/lib/utils/utils.py index 2cc322a29..e5f31b86e 100644 --- a/r2/r2/lib/utils/utils.py +++ b/r2/r2/lib/utils/utils.py @@ -26,6 +26,7 @@ from copy import deepcopy import cPickle as pickle import re, datetime, math, random, string, sha +from datetime import datetime, timedelta from pylons.i18n import ungettext, _ @@ -369,7 +370,7 @@ def timeago(interval): month = 60 * 60 * 24 * 30, year = 60 * 60 * 24 * 365)[period] delta = num * d - return datetime.datetime.now(g.tz) - datetime.timedelta(0, delta) + return datetime.now(g.tz) - timedelta(0, delta) def timetext(delta, resultion = 1, bare=True): """ @@ -386,7 +387,7 @@ def timetext(delta, resultion = 1, bare=True): (60, lambda n: ungettext('minute', 'minutes', n)), (1, lambda n: ungettext('second', 'seconds', n)) ) - delta = max(delta, datetime.timedelta(0)) + delta = max(delta, timedelta(0)) since = delta.days * 24 * 60 * 60 + delta.seconds for i, (seconds, name) in enumerate(chunks): count = math.floor(since / seconds) @@ -394,7 +395,7 @@ def timetext(delta, resultion = 1, bare=True): break from r2.lib.strings import strings - if count == 0 and delta.seconds == 0 and delta != datetime.timedelta(0): + if count == 0 and delta.seconds == 0 and delta != timedelta(0): n = math.floor(delta.microseconds / 1000) s = strings.number_label % (n, ungettext("millisecond", "milliseconds", n)) @@ -412,11 +413,11 @@ def timetext(delta, resultion = 1, bare=True): def timesince(d, resultion = 1, bare = True): from pylons import g - return timetext(datetime.datetime.now(g.tz) - d) + return timetext(datetime.now(g.tz) - d) def timeuntil(d, resultion = 1, bare = True): from pylons import g - return timetext(d - datetime.datetime.now(g.tz)) + return timetext(d - datetime.now(g.tz)) def to_base(q, alphabet): @@ -667,7 +668,7 @@ def find_broken_things(cls,attrs,time,delete = False): for Things of that class, missing those attributes, deleting them if requested """ - for t in fetch_things(cls,time,datetime.datetime.now()): + for t in fetch_things(cls,time,datetime.now()): for a in attrs: try: # try to retreive the attribute @@ -694,7 +695,6 @@ def timeit(func): def lineno(): "Returns the current line number in our program." import inspect - from datetime import datetime print "%s\t%s" % (datetime.now(),inspect.currentframe().f_back.f_lineno) class IteratorChunker(object): diff --git a/r2/r2/models/account.py b/r2/r2/models/account.py index 3a56512ae..86bb94e71 100644 --- a/r2/r2/models/account.py +++ b/r2/r2/models/account.py @@ -57,9 +57,9 @@ class Account(Thing): report_ignored = 0, spammer = 0, sort_options = {}, - has_subscribed = False + has_subscribed = False, ) - + def karma(self, kind, sr = None): suffix = '_' + kind + '_karma'