Use cross domain https for slightly safer login.

Login UI code has been simplified and moved into the client side. CORS
is used for the cross-domain POST if available, otherwise an iframe and
cookie polling technique is used. Start fleshing out the new JS tree. :)
This commit is contained in:
Max Goodman
2011-10-06 07:21:04 -07:00
parent edbefbcf4f
commit b847757069
29 changed files with 513 additions and 196 deletions

View File

@@ -22,7 +22,7 @@
# Javascript files to be compressified
js_libs = $(addprefix lib/,json2.js jquery.cookie.js jquery.url.js ui.core.js ui.datepicker.js jquery.flot.js jquery.lazyload.js)
js_sources = $(js_libs) jquery.reddit.js reddit.js base.js sponsored.js compact.js blogbutton.js flair.js analytics.js
js_sources = $(js_libs) jquery.reddit.js reddit.js login.js ui.js base.js sponsored.js compact.js blogbutton.js flair.js analytics.js
js_targets = button.js jquery.flot.js sponsored.js
localized_js_targets = reddit.js mobile.js
localized_js_outputs = $(localized_js_targets:.js=.*.js)

View File

@@ -85,6 +85,8 @@ display_timezone = MST
shutdown_secret = 12345
# list of servers that the service monitor will care about
monitored_servers = reddit, localhost
# https api endpoint (must be g.domain or a subdomain of g.domain)
https_endpoint =
# name of the cookie to drop with login information
login_cookie = reddit_session

View File

@@ -20,7 +20,7 @@
# CondeNet, Inc. All Rights Reserved.
################################################################################
from reddit_base import RedditController, MinimalController, set_user_cookie
from reddit_base import paginated_listing
from reddit_base import cross_domain, paginated_listing
from pylons.i18n import _
from pylons import c, request, response
@@ -110,7 +110,7 @@ class ApiController(RedditController):
@json_validate()
def GET_me(self):
def GET_me(self, responder):
if c.user_is_loggedin:
return Wrapped(c.user).render()
else:
@@ -353,60 +353,53 @@ class ApiController(RedditController):
else:
form.set_html(".title-status", _("no title found"))
def _login(self, form, user, dest='', rem = None):
def _login(self, responder, user, rem = None):
"""
AJAX login handler, used by both login and register to set the
user cookie and send back a redirect.
"""
self.login(user, rem = rem)
form._send_data(modhash = user.modhash())
form._send_data(cookie = user.make_cookie())
dest = dest or request.referer or '/'
form.redirect(dest)
if request.params.get("hoist") != "cookie":
responder._send_data(modhash = user.modhash())
responder._send_data(cookie = user.make_cookie())
@cross_domain([g.origin, g.https_endpoint], allow_credentials=True)
@validatedForm(VDelay("login"),
user = VLogin(['user', 'passwd']),
username = VLength('user', max_length = 100),
dest = VDestination(),
rem = VBoolean('rem'),
reason = VReason('reason'))
def POST_login(self, form, jquery, user, username, dest, rem, reason):
if form.has_errors('vdelay', errors.RATELIMIT):
jquery(".recover-password").addClass("attention")
rem = VBoolean('rem'))
def POST_login(self, form, responder, user, username, rem):
if responder.has_errors('vdelay', errors.RATELIMIT):
return
if reason and reason[0] == 'redirect':
dest = reason[1]
if login_throttle(username, wrong_password = form.has_errors("passwd",
if login_throttle(username, wrong_password = responder.has_errors("passwd",
errors.WRONG_PASSWORD)):
VDelay.record_violation("login", seconds=1, growfast=True)
jquery(".recover-password").addClass("attention")
c.errors.add(errors.WRONG_PASSWORD, field = "passwd")
if not form.has_errors("passwd", errors.WRONG_PASSWORD):
self._login(form, user, dest, rem)
if not responder.has_errors("passwd", errors.WRONG_PASSWORD):
self._login(responder, user, rem)
@cross_domain([g.origin, g.https_endpoint], allow_credentials=True)
@validatedForm(VCaptcha(),
VRatelimit(rate_ip = True, prefix = "rate_register_"),
name = VUname(['user']),
email = ValidEmails("email", num = 1),
password = VPassword(['passwd', 'passwd2']),
dest = VDestination(),
rem = VBoolean('rem'),
reason = VReason('reason'))
def POST_register(self, form, jquery, name, email,
password, dest, rem, reason):
if not (form.has_errors("user", errors.BAD_USERNAME,
rem = VBoolean('rem'))
def POST_register(self, form, responder, name, email,
password, rem):
bad_captcha = responder.has_errors('captcha', errors.BAD_CAPTCHA)
if not (responder.has_errors("user", errors.BAD_USERNAME,
errors.USERNAME_TAKEN_DEL,
errors.USERNAME_TAKEN) or
form.has_errors("email", errors.BAD_EMAILS) or
form.has_errors("passwd", errors.BAD_PASSWORD) or
form.has_errors("passwd2", errors.BAD_PASSWORD_MATCH) or
form.has_errors('ratelimit', errors.RATELIMIT) or
(not g.disable_captcha and form.has_errors('captcha', errors.BAD_CAPTCHA))):
responder.has_errors("email", errors.BAD_EMAILS) or
responder.has_errors("passwd", errors.BAD_PASSWORD) or
responder.has_errors("passwd2", errors.BAD_PASSWORD_MATCH) or
responder.has_errors('ratelimit', errors.RATELIMIT) or
(not g.disable_captcha and bad_captcha)):
user = register(name, password)
VRatelimit.ratelimit(rate_ip = True, prefix = "rate_register_")
@@ -427,14 +420,7 @@ class ApiController(RedditController):
user._commit()
c.user = user
if reason:
if reason[0] == 'redirect':
dest = reason[1]
elif reason[0] == 'subscribe':
for sr, sub in reason[1].iteritems():
self._subscribe(sr, sub)
self._login(form, user, dest, rem)
self._login(responder, user, rem)
@noresponse(VUser(),
VModhash(),

View File

@@ -19,7 +19,7 @@
# All portions of the code written by CondeNet are Copyright (c) 2006-2010
# CondeNet, Inc. All Rights Reserved.
################################################################################
from pylons import c, request, g
from pylons import c, g, request, response
from pylons.i18n import _
from pylons.controllers.util import abort
from r2.lib import utils, captcha, promote
@@ -154,14 +154,15 @@ def api_validate(response_type=None):
def _api_validate(*simple_vals, **param_vals):
def val(fn):
def newfn(self, *a, **env):
c.render_style = api_type(request.params.get("renderstyle",
response_type))
c.response_content_type = 'application/json; charset=UTF-8'
c.render_style = api_type(request.params.get("renderstyle", response_type))
# generate a response object
if response_type is None or request.params.get('api_type') == "json":
responder = JsonResponse()
else:
if response_type == "html" and not request.params.get('api_type') == "json":
responder = JQueryResponse()
else:
responder = JsonResponse()
c.response_content_type = responder.content_type
try:
kw = _make_validated_kw(fn, simple_vals, param_vals, env)
return response_function(self, fn, responder,
@@ -191,8 +192,11 @@ def textresponse(self, self_method, responder, simple_vals, param_vals, *a, **kw
def json_validate(self, self_method, responder, simple_vals, param_vals, *a, **kw):
if c.extension != 'json':
abort(404)
r = self_method(self, *a, **kw)
return self.api_wrapper(r)
val = self_method(self, responder, *a, **kw)
if not val:
val = responder.make_response()
return self.api_wrapper(val)
@api_validate("html")
def validatedForm(self, self_method, responder, simple_vals, param_vals,
@@ -1234,38 +1238,6 @@ class VOneOf(Validator):
else:
return val
class VReason(Validator):
def run(self, reason):
if not reason:
return
if reason.startswith('redirect_'):
dest = reason[9:]
if (not dest.startswith(c.site.path) and
not dest.startswith("http:")):
dest = (c.site.path + dest).replace('//', '/')
return ('redirect', dest)
if reason.startswith('vote_'):
fullname = reason[5:]
t = Thing._by_fullname(fullname, data=True)
return ('redirect', t.make_permalink_slow())
elif reason.startswith('share_'):
fullname = reason[6:]
t = Thing._by_fullname(fullname, data=True)
return ('redirect', t.make_permalink_slow())
elif reason.startswith('reply_'):
fullname = reason[6:]
t = Thing._by_fullname(fullname, data=True)
return ('redirect', t.make_permalink_slow())
elif reason.startswith('sr_change_'):
sr_list = reason[10:].split(',')
fullnames = dict(i.split(':') for i in sr_list)
srs = Subreddit._by_fullname(fullnames.keys(), data = True,
return_dict = False)
sr_onoff = dict((sr, fullnames[sr._fullname] == 1) for sr in srs)
return ('subscribe', sr_onoff)
class ValidEmails(Validator):
"""Validates a list of email addresses passed in as a string and
delineated by whitespace, ',' or ';'. Also validates quantity of

View File

@@ -286,6 +286,8 @@ class Globals(object):
self.origin = "http://" + self.domain
self.secure_domains = set([urlparse(self.payment_domain).netloc])
if self.https_endpoint:
self.secure_domains.add(urlparse(self.https_endpoint).netloc)
# load the unique hashed names of files under static
static_files = os.path.join(self.paths.get('static_files'), 'static')

View File

@@ -187,6 +187,8 @@ module["reddit"] = LocalizedModule("reddit.js",
"lib/jquery.url.js",
"jquery.reddit.js",
"base.js",
"ui.js",
"login.js",
"analytics.js",
"flair.js",
"reddit.js",

View File

@@ -44,6 +44,9 @@ class JsonResponse(object):
in the api func's validators, as well as blobs of data set by the
api func.
"""
content_type = 'application/json; charset=UTF-8'
def __init__(self):
self._clear()
@@ -67,7 +70,7 @@ class JsonResponse(object):
res = {}
if self._data:
res['data'] = self._data
res['errors'] = [(e[0], c.errors[e].message) for e in self._errors]
res['errors'] = [(e[0], c.errors[e].message, e[1]) for e in self._errors]
return {"json": res}
def set_error(self, error_name, field_name):

View File

@@ -44,6 +44,8 @@ string_dict = dict(
banned_by = "removed by %s",
banned = "removed",
reports = "reports: %d",
submitting = _("submitting..."),
# this accomodates asian languages which don't use spaces
number_label = _("%(num)d %(thing)s"),

View File

@@ -0,0 +1 @@
../throbber.gif

View File

@@ -377,6 +377,10 @@ body[orient="landscape"] > #topbar > h1 { margin-left: -125px; width: 250px; }
.loading img { -webkit-animation-name: rotateThis; -webkit-animation-duration: .5s; -webkit-animation-iteration-count: infinite; -webkit-animation-timing-function: linear; }
.throbber { display: none; margin: 0 2px; background: url("/static/compact/throbber.gif") no-repeat; width: 18px; height: 18px; }
.working .throbber { display: inline-block; }
/* Login and Register */
#login_login, #login_reg { background: white; border: 1px solid #d9d9d9; margin: 10px; -webkit-border-bottom-left-radius: 8px; -webkit-border-bottom-right-radius: 8px; -moz-border-radius-bottomleft: 8px; -moz-border-radius-bottomright: 8px; max-width: 350px; margin-left: auto; margin-right: auto; }
@@ -390,6 +394,8 @@ body[orient="landscape"] > #topbar > h1 { margin-left: -125px; width: 250px; }
#login_login > div > ul li input[type="checkbox"] + label, #login_reg > div > ul li input[type="checkbox"] + label { display: inline; }
.user-form .submit * { vertical-align: middle; }
/* toolbar specific stuf here */
body.toolbar { margin: 0px; padding: 0px; overflow: hidden; }

View File

@@ -1097,6 +1097,16 @@ padding: 5px;
-webkit-animation-iteration-count:infinite;
-webkit-animation-timing-function:linear;
}
.throbber {
display: none;
margin: 0 2px;
background: url($static + 'throbber.gif') no-repeat;
width: 18px;
height: 18px;
}
.working .throbber { display: inline-block; }
/* Login and Register */
#login_login, #login_reg {
background: white;
@@ -1133,6 +1143,11 @@ padding: 5px;
#login_login > div > ul li input[type="checkbox"] + label, #login_reg > div > ul li input[type="checkbox"] + label {
display: inline;
}
.user-form .submit * {
vertical-align: middle;
}
/* toolbar specific stuf here */
body.toolbar {
margin: 0px;

View File

@@ -1412,21 +1412,36 @@ textarea.gray { color: gray; }
border: 1px solid gray;
}
.login-form-side input {
border: 1px solid gray;
width: 138px;
.login-form-side input[type=text],
.login-form-side input[type=password] {
border: 1px solid #999;
width: 137px;
height: 17px;
margin: 5px 0px 0px 5px;
top: 5px;
padding: 1px;
}
.login-form-side .error {
margin: 5px;
.login-form-side input[type=password] {
width: 138px;
}
#remember-me {
margin: 5px;
.login-form-side #remember-me,
.login-form-side .submit {
margin: 4px;
}
.login-form-side .submit input[type=button] {
margin:1px;
}
.login-form-side #remember-me {
float: left;
line-height: 24px;
}
.login-form-side #remember-me * {
vertical-align:middle;
}
/*the checkbox*/
@@ -1436,6 +1451,7 @@ textarea.gray { color: gray; }
width: auto;
border: none;
margin-right: 5px;
margin-top: 0;
}
.login-form-side label {
@@ -1444,13 +1460,31 @@ textarea.gray { color: gray; }
white-space: nowrap;
}
.login-form-side button {
.login-form-side .recover-password {
margin-left: 1em;
}
.login-form-side .status { display:none; }
.login-form-side .submit {
float: right;
}
.login-form-side .submit *, .user-form .submit * {
vertical-align: middle;
}
.status { margin-left: 5px; color: red; font-size: small;}
.error { color: red; font-size: small; margin: 5px; }
.throbber {
display: none;
margin: 0 2px;
background: url(/static/throbber.gif) no-repeat;
width: 18px;
height: 18px;
}
.working .throbber { display: inline-block; }
.status { margin: 5px 0 0 5px; font-size: small;}
.error { color: red; font-size: small; }
.red { color:red }
.buygold { color: #9A7D2E; font-weight: bold; }
.line-through { text-decoration: line-through }
@@ -1572,16 +1606,16 @@ label + #moresearchinfo {
.legal {color: #808080; font-family: serif; font-size: small; margin-top: 20px; }
.legal a {text-decoration: underline}
.divide { border-right: 2px solid #D3D3D3; }
.divide { border-right: 2px solid #D3D3D3; margin-right: -2px; }
.loginform {
.login-form-section {
float: left;
width: 45%;
padding-left: 15px;
padding-right: 15px;
width: 46%;
padding-left: 2%;
padding-right: 2%;
}
.loginform h3 {
.login-form-section h3 {
margin-bottom: 0;
margin-top: 10px;
font-size: large;
@@ -1589,26 +1623,31 @@ label + #moresearchinfo {
font-variant: small-caps;
color: #404040;
}
.loginform p {
.login-form-section p {
text-align: left;
margin-bottom: 10px;
color: #606060;
margin-bottom: 20px;
}
.loginform label {
.user-form label {
display: block;
font-weight: bold;
color: #606060;
}
.loginform .remember { display:inline; margin-left: 5px; }
.loginform ul { margin: 5px; }
.loginform li { margin-top: 5px; }
.loginform p .btn { margin-top: 5px }
.loginform input.logtxt { width: 125px; }
.user-form .error {
margin-left: 5px;
}
.loginform input[type=text],
.loginform input[type=password] {
.user-form .remember { display:inline; margin-left: 5px; }
.user-form ul { margin: 5px; }
.user-form li { margin-top: 5px; }
.user-form p .btn { margin-top: 5px }
.user-form input.logtxt { width: 125px; }
.user-form input[type=text],
.user-form input[type=password] {
width: 125px;
border: 1px solid #A0A0A0;
margin-top: 2px;
@@ -1616,10 +1655,14 @@ label + #moresearchinfo {
padding: 1px;
}
.loginform #captcha {
.user-form #captcha {
width: 250px;
}
.user-form .submit {
margin-top: 10px;
}
#passform h1 {margin-bottom: 0px}
#passform p {margin-bottom: 5px; font-size: small}
@@ -1664,6 +1707,11 @@ label + #moresearchinfo {
font-weight: normal;
}
.popup .close-popup {
text-align: center;
margin-top: 10px;
}
.usertable { margin-left: 10px;}
.usertable { font-size: larger }
.usertable td, .usertable th { padding: 0 .7em }

View File

@@ -1 +1,5 @@
r = {}
r = r || {}
$(function() {
r.login.ui.init()
})

View File

@@ -140,7 +140,6 @@ function showcover() {
$(".login-popup:first").fadeIn()
.find(".popup").css("top", $(window).scrollTop() + 75).end()
.find(".cover").css("height", $(document).height()).end()
.find("form input[name=reason]").attr("value", (reason || ""));
return false;
}

View File

@@ -259,10 +259,7 @@ rate_limit = function() {
$.fn.vote = function(vh, callback, event, ui_only) {
/* for vote to work, $(this) should be the clicked arrow */
if (!reddit.logged) {
showcover(true, 'vote_' + $(this).thing_id());
}
else if($(this).hasClass("arrow")) {
if (reddit.logged && $(this).hasClass("arrow")) {
var dir = ( $(this).hasClass(up_cls) ? 1 :
( $(this).hasClass(down_cls) ? -1 : 0) );
var things = $(this).all_things_by_id();
@@ -303,9 +300,6 @@ $.fn.vote = function(vh, callback, event, ui_only) {
if(callback)
callback(things, dir);
}
if(event) {
event.stopPropagation();
}
};
$.fn.show_unvotable_message = function() {
@@ -584,8 +578,7 @@ $.fn.captcha = function(iden) {
/* */
var c = this.find(".capimage");
if(iden) {
c.attr("src", "http://" + reddit.ajax_domain
+ "/captcha/" + iden + ".png")
c.attr("src", "/captcha/" + iden + ".png")
.parents("form").find('input[name="iden"]').val(iden);
}
return c;

View File

@@ -0,0 +1,211 @@
r.login = {
post: function(form, action, callback) {
var username = $('input[name="user"]', form.$el).val(),
endpoint = r.config.https_endpoint || ('http://'+r.config.ajax_domain),
sameOrigin = location.protocol+'//'+location.host == endpoint,
apiTarget = endpoint+'/api/'+action+'/'+username
if (sameOrigin || $.support.cors) {
var params = form.serialize()
params.push({name:'api_type', value:'json'})
$.ajax({
url: apiTarget,
type: 'POST',
dataType: 'json',
data: params,
success: callback,
error: function(xhr, err) {
callback(false, err, xhr)
},
xhrFields: {
withCredentials: true
}
})
} else {
var iframe = $('<iframe>'),
postForm = form.$el.clone(true)
iframe
.css('display', 'none')
.appendTo('body')
var frameName = iframe[0].contentWindow.name = ('resp'+Math.random()).replace('.', '')
postForm
.unbind()
.css('display', 'none')
.attr('action', apiTarget)
.attr('target', frameName)
.appendTo('body')
$('<input>')
.attr({
type: 'hidden',
name: 'api_type',
value: 'json'
})
.appendTo(postForm)
$('<input>')
.attr({
type: 'hidden',
name: 'hoist',
value: r.login.hoist.type
})
.appendTo(postForm)
r.login.hoist.watch(action, function(result) {
if (!r.config.debug) {
iframe.remove()
postForm.remove()
}
callback(result)
})
postForm.submit()
}
}
}
r.login.hoist = {
type: 'cookie',
watch: function(name, callback) {
var cookieName = 'hoist_'+name
var interval = setInterval(function() {
data = $.cookie(cookieName)
if (data) {
try {
data = JSON.parse(data)
} catch(e) {
data = null
}
$.cookie(cookieName, null, {domain:r.config.cur_domain})
clearInterval(interval)
callback(data)
}
}, 100)
}
}
r.login.ui = {
init: function() {
if (!r.config.logged) {
$('.content form.login-form, .side form.login-form').each(function(i, el) {
new r.ui.LoginForm(el)
})
$('.content form.register-form').each(function(i, el) {
new r.ui.RegisterForm(el)
})
this.popup = new r.ui.LoginPopup($('.login-popup')[0])
$(document).delegate('.login-required', 'click', $.proxy(this, 'loginRequiredAction'))
}
},
loginRequiredAction: function(e) {
if (r.config.logged) {
return true
} else {
var el = $(e.target),
href = el.attr('href'),
dest
if (href && href != '#') {
// User clicked on a link that requires login to continue
dest = href
} else {
// User clicked on a thing button that requires login
var thing = el.thing()
if (thing.length) {
dest = thing.find('.comments').attr('href')
}
}
this.popup.show(true, dest && function() {
window.location = dest
})
return false
}
}
}
r.ui.LoginForm = function() {
r.ui.Form.apply(this, arguments)
}
r.ui.LoginForm.prototype = $.extend(new r.ui.Form(), {
showErrors: function(errors) {
r.ui.Form.prototype.showErrors.call(this, errors)
if (errors.length) {
this.$el.find('.recover-password')
.addClass('attention')
}
},
showStatus: function() {
this.$el.find('.error').css('opacity', 1)
r.ui.Form.prototype.showStatus.apply(this, arguments)
},
resetErrors: function() {
if (this.$el.hasClass('login-form-side')) {
// Dim the error in place so the form doesn't change size.
var errorEl = this.$el.find('.error')
if (errorEl.is(':visible')) {
errorEl.fadeTo(100, .35)
}
} else {
r.ui.Form.prototype.resetErrors.apply(this, arguments)
}
},
_submit: function() {
r.login.post(this, 'login', $.proxy(this, 'handleResult'))
},
_handleResult: function(result) {
if (!result.json.errors.length) {
// Success. Load the destination page with the new session cookie.
if (this.successCallback) {
this.successCallback(result)
} else {
var base = r.config.extension ? '/.'+r.config.extension : '/',
defaultDest = /\/login\/?$/.test($.url().attr('path')) ? base : window.location,
destParam = this.$el.find('input[name="dest"]').val()
window.location = destParam || defaultDest
}
} else {
r.ui.Form.prototype._handleResult.call(this, result)
}
}
})
r.ui.RegisterForm = function() {
r.ui.Form.apply(this, arguments)
}
r.ui.RegisterForm.prototype = $.extend(new r.ui.Form(), {
_submit: function() {
r.login.post(this, 'register', $.proxy(this, 'handleResult'))
},
_handleResult: r.ui.LoginForm.prototype._handleResult
})
r.ui.LoginPopup = function(el) {
r.ui.Base.call(this, el)
this.loginForm = new r.ui.LoginForm(this.$el.find('form.login-form:first'))
this.registerForm = new r.ui.RegisterForm(this.$el.find('form.register-form:first'))
}
r.ui.LoginPopup.prototype = $.extend(new r.ui.Base(), {
show: function(notice, callback) {
this.loginForm.successCallback = callback
this.registerForm.successCallback = callback
$.request("new_captcha", {id: this.$el.attr('id')})
this.$el
.find(".cover-msg").toggle(!!notice).end()
.show()
}
})

View File

@@ -123,17 +123,6 @@ function showlang() {
return false;
};
function showcover(warning, reason) {
$.request("new_captcha");
if (warning)
$("#cover_disclaim, #cover_msg").show();
else
$("#cover_disclaim, #cover_msg").hide();
$(".login-popup:first").show()
.find('form input[name="reason"]').val(reason || "");
return false;
};
function hidecover(where) {
$(where).parents(".cover-overlay").hide();
return false;
@@ -322,9 +311,7 @@ function linkstatus(form) {
function subscribe(reddit_name) {
return function() {
if (!reddit.logged) {
showcover();
} else {
if (reddit.logged) {
$.things(reddit_name).find(".entry").addClass("likes");
$.request("subscribe", {sr: reddit_name, action: "sub"});
}
@@ -333,9 +320,7 @@ function subscribe(reddit_name) {
function unsubscribe(reddit_name) {
return function() {
if (!reddit.logged) {
showcover();
} else {
if (reddit.logged) {
$.things(reddit_name).find(".entry").removeClass("likes");
$.request("subscribe", {sr: reddit_name, action: "unsub"});
}
@@ -344,10 +329,7 @@ function unsubscribe(reddit_name) {
function friend(user_name, container_name, type) {
return function() {
if (!reddit.logged) {
showcover();
}
else {
if (reddit.logged) {
encoded = encodeURIComponent(reddit.referer);
$.request("friend?note=" + encoded,
{name: user_name, container: container_name, type: type});

View File

@@ -0,0 +1,104 @@
r.ui = {}
r.ui.Base = function(el) {
this.$el = $(el)
}
r.ui.Form = function(el) {
r.ui.Base.call(this, el)
this.$el.submit($.proxy(function(e) {
e.preventDefault()
this.submit(e)
}, this))
}
r.ui.Form.prototype = $.extend(new r.ui.Base(), {
workingDelay: 200,
setWorking: function(isWorking) {
// Delay the initial throbber display to prevent flashes for fast
// operations
if (isWorking) {
if (!this.$el.hasClass('working') && !this._workingTimer) {
this._workingTimer = setTimeout($.proxy(function() {
this.$el.addClass('working')
}, this), this.workingDelay)
}
} else {
if (this._workingTimer) {
clearTimeout(this._workingTimer)
delete this._workingTimer
}
this.$el.removeClass('working')
}
},
showStatus: function(msg, isError) {
this.$el.find('.status')
.show()
.toggleClass('error', !!isError)
.text(msg)
},
showErrors: function(errors) {
statusMsgs = []
$.each(errors, $.proxy(function(i, err) {
var errName = err[0],
errMsg = err[1],
errField = err[2],
errCls = '.error.'+errName + (errField ? '.field-'+errField : ''),
errEl = this.$el.find(errCls)
if (errEl.length) {
errEl.show().text(errMsg)
} else {
statusMsgs.push(errMsg)
}
}, this))
if (statusMsgs.length) {
this.showStatus(statusMsgs.join(', '), true)
}
},
resetErrors: function() {
this.$el.find('.error').hide()
},
checkCaptcha: function(errors) {
if (this.$el.has('input[name="captcha"]').length) {
var badCaptcha = $.grep(errors, function(el) {
return el[0] == 'badCaptcha'
})
if (badCaptcha) {
$.request("new_captcha", {id: this.$el.attr('id')})
}
}
},
serialize: function() {
return this.$el.serializeArray()
},
submit: function() {
this.resetErrors()
this.setWorking(true)
this._submit()
},
_submit: function() {},
handleResult: function(result, err, xhr) {
if (result) {
this.checkCaptcha(result.json.errors)
this._handleResult(result)
} else {
this.setWorking(false)
this.showStatus('an error occurred (' + xhr.status + ')', true)
}
},
_handleResult: function(result) {
this.showErrors(result.json.errors)
this.setWorking(false)
}
})

View File

@@ -79,10 +79,8 @@ ${rounded_captcha()}
<td>
%endif
<input class="captcha cap-text" id="captcha_"
name="captcha" type="text" size="${size}" />
<script type="text/javascript">
emptyInput($('input.captcha'), "${_('type the letters from the image above')}");
</script>
name="captcha" type="text" size="${size}"
placeholder="type the letters from the image above" />
%if tabular:
</td>
<td>

View File

@@ -46,18 +46,12 @@
<%def name="login_form(register=False, user='', dest='', include_tos=True)">
<% op = "reg" if register else "login" %>
<form id="login_${op}" method="post"
action="${add_sr('/post/' + op, nocname = True)}"
%if not c.frameless_cname or c.authorized_cname:
onsubmit="return post_user(this, '${"register" if register else "login"}');"
%else:
onsubmit="return update_user(this);"
%endif
target="_top">
action="${add_sr(g.https_endpoint + '/post/' + op, nocname = True)}"
class="user-form ${'register-form' if register else 'login-form'}">
%if c.cname:
<input type="hidden" name="${UrlParser.cname_get}"
value="${random.random()}" />
%endif
<input type="hidden" name="reason" value="" />
<input type="hidden" name="op" value="${op}" />
%if dest:
<input type="hidden" name="dest" value="${dest}" />
@@ -126,10 +120,11 @@
</li>
%endif
</ul>
<p>
<p class="submit">
<button type="submit" class="button">
${register and _("create account") or _("login")}
</button>
<span class="throbber"></span>
<span class="status"></span>
</p>
</div>
@@ -138,7 +133,7 @@
<%def name="login_panel(lf, user_reg = '', user_login = '', dest='')">
<div class="loginform divide">
<div class="login-form-section divide">
<h3>${_("create a new account")}</h3>
<p class="tagline">
${_("all it takes is a username and password")}
@@ -150,7 +145,7 @@
</span>
</p>
</div>
<div class="loginform">
<div class="login-form-section">
<h3>${_("login")}</h3>
<p class="tagline">
${_("already have an account and just want to login?")}

View File

@@ -28,33 +28,31 @@
<%namespace file="utils.html" import="error_field"/>
<% op = "login-main" %>
<form method="post"
id="login_${op}" action="${add_sr('/post/login', nocname = True)}"
%if thing.auth_cname:
onsubmit="return post_user(this, 'login');"
%else:
onsubmit="return update_user(this);"
%endif
class="login-form-side">
<form method="post"
action="${add_sr(g.https_endpoint + '/post/' + op, nocname = True)}"
id="login_${op}"
class="login-form login-form-side">
%if thing.cname:
<input type="hidden" name="${UrlParser.cname_get}"
value="${random.random()}" />
%endif
<input type="hidden" name="op" value="${op}" />
<input name="user" type="text" maxlength="20" tabindex="1"/>
<input name="passwd" type="password" maxlength="20" tabindex="2"/>
<input name="user" placeholder="username" type="text" maxlength="20" tabindex="1"/>
<input name="passwd" placeholder="password" type="password" maxlength="20" tabindex="2"/>
${error_field("WRONG_PASSWORD", "passwd", "div")}
${error_field("RATELIMIT", "ratelimit")}
${error_field("RATELIMIT", "vdelay")}
<div class="status error"></div>
<div class="status"></div>
<div id="remember-me">
<button class="btn" type="submit" tabindex="4">${_("login")}</button>
<input type="checkbox" name="rem" tabindex="3" id="rem-${op}" />
<label for="rem-${op}">${_("remember me")}</label>
<a class="recover-password" href="/password">${_("recover password")}</a>
<div class="clear"></div>
<a class="recover-password" href="/password">${_("reset password")}</a>
</div>
<div class="submit">
<span class="throbber"></span>
<button class="btn" type="submit" tabindex="4">${_("login")}</button>
</div>
<div class="clear"></div>
</form>

View File

@@ -123,7 +123,7 @@ ${self.RenderPrintable()}
_class = _type + ("mod" if mod else "")
fullname = this._fullname
%>
<div class="arrow ${_class}"
<div class="arrow ${_class} login-required"
%if getattr(thing, "votable", True):
onclick="$(this).vote('${thing.votehash}', null, event)"
%else:

View File

@@ -424,12 +424,11 @@
callback, cancelback = cancelback, callback
title, alt_title = alt_title, title
css_class, alt_css_class = alt_css_class, css_class
extra_class = "login-required" if login_required else ""
%>
<span class="${class_name} toggle" style="${style}">
<a class="option active ${css_class}" href="#" tabindex="100"
%if login_required and not c.user_is_loggedin:
onclick="return showcover('', '${callback}_' + $(this).thing_id());"
%else:
<a class="option active ${css_class} ${extra_class}" href="#" tabindex="100"
%if not login_required or c.user_is_loggedin:
onclick="return toggle(this, ${callback}, ${cancelback})"
%endif
>

View File

@@ -54,7 +54,7 @@
"friend('%s', '%s', 'friend')" % (thing.user.name, thing.my_fullname),
"unfriend('%s', '%s', 'friend')" % (thing.user.name, thing.my_fullname),
css_class = "add", alt_css_class = "remove",
reverse = thing.is_friend)}
reverse = thing.is_friend, login_required=True)}
</div>
%endif

View File

@@ -149,7 +149,7 @@
<div class="popup">
<h1 class="cover-msg">${strings.cover_msg}</h1>
${login_panel(login_form)}
<div style="text-align:center; clear: both">
<div class="close-popup">
<a href="#" onclick="return hidecover(this)">
${_("close this window")}
</a>

View File

@@ -63,7 +63,7 @@
%if thing.enable_login_cover and not g.read_only_mode:
<span class="user">
${text_with_js(_("want to join? %(register)s in seconds"),
register=(_("register"), "return showcover(false);"))}
register=(_("register"), "r.login.ui.popup.show(); return false"))}
</span>
${separator("|")}
%endif

View File

@@ -23,25 +23,16 @@
<%namespace file="utils.html" import="plain_link"/>
<div class="sidebox ${thing.css_class}">
<%
if not c.user_is_loggedin and thing.show_cover:
onclick="return(showcover(true, 'redirect_%s'));" % thing.link
else:
onclick = None
nocname = thing.nocname
%>
<div class="morelink">
${plain_link(thing.title,thing.link, _sr_path = thing.sr_path,
onclick=onclick, nocname=nocname)}
${plain_link(thing.title, thing.link, _sr_path=thing.sr_path,
_class='login-required', nocname=thing.nocname)}
<div class="nub"> </div>
</div>
%if thing.subtitles:
<div class="spacer">
${plain_link('', thing.link, _sr_path=thing.sr_path,
onclick=onclick, nocname=nocname)}
${plain_link('', thing.link, _sr_path=thing.sr_path,
_class='login-required', nocname=thing.nocname)}
%for subtitle in thing.subtitles:
<div class="subtitle">${subtitle}</div>
%endfor

View File

@@ -65,7 +65,8 @@
"subscribe('%s')" % sr._fullname,
"unsubscribe('%s')" % sr._fullname,
css_class = "add", alt_css_class = "remove",
reverse = sr.subscriber)}
reverse = sr.subscriber,
login_required = True)}
</%def>
##this function is used by subscriptionbox.html

View File

@@ -365,7 +365,8 @@ ${unsafe(txt)}
<%
from r2.lib.template_helpers import get_domain
%>
var reddit = {
r = {};
r.config = reddit = {
/* is the user logged in */
logged: ${c.user_is_loggedin and ("'%s'" % c.user.name) or "false"},
/* the subreddit's name (for posts) */
@@ -383,6 +384,8 @@ ${unsafe(txt)}
no_www = True)}",
/* where do ajax request go? */
ajax_domain: "${get_domain(cname=c.authorized_cname, subreddit = False)}",
extension: '${c.extension}',
https_endpoint: "${g.https_endpoint}",
/* debugging? */
debug: ${"true" if g.debug else "false"},
vl: {},