tiddlywiki
Version:
a non-linear personal web notebook
766 lines (744 loc) • 22.1 kB
JavaScript
/***
https://raw.github.com/TiddlySpace/ts.js/master/src/ts.js
***/
//{{{
/*jslint vars: true, browser: true */
/*global jQuery, tiddlyweb, confirm, prompt */
//
// version 0.5.12
(function($) {
"use strict";
var getCSRFToken = function() {
// XXX: should not use RegEx - cf.
// http://www.quirksmode.org/js/cookies.html
// https://github.com/TiddlySpace/tiddlyspace/commit/5f4adbe009ed4bda3ce39058a3fb07de1420358d
var regex = /^(?:.*; )?csrf_token=([^(;|$)]*)(?:;|$)/;
var match = regex.exec(document.cookie);
var csrf_token = null;
if (match && (match.length === 2)) {
csrf_token = match[1];
}
return csrf_token;
};
$.ajaxSetup({
beforeSend: function(xhr) {
xhr.setRequestHeader("X-ControlView", "false");
}
});
window.getCSRFToken = getCSRFToken;
/*
* Add the members of spaceName to the current space.
*/
function addMembersfromSpace(ts, spaceName, callback, errback) {
new tiddlyweb.Space(spaceName, "/").members().get(function(members) {
var spaceMembers = new tiddlyweb.Space(ts.currentSpace, "/").
members(),
putMembers = function(members, callback, errback) {
var currentMember = members.shift();
if (currentMember) {
var next = function() {
putMembers(members, callback, errback);
};
spaceMembers.add(currentMember, next, errback);
} else {
callback();
}
};
putMembers(members, callback, errback);
}, errback);
}
/*
* Reset the message area associated with the provided form.
*/
function resetMessage(form) {
$(".annotation", form).removeClass("annotation");
$(".messageArea", form).removeClass("error").hide();
$(".inputArea", form).show();
}
/*
* Build message area element
*/
function buildMsgArea(form) {
var msgArea = $("<div class='messageArea' />");
msgArea
.append($("<button></button>")
.html("×")
.addClass("close-btn")
.attr("title", "close notification")
.data("parent-class", "messageArea")
.click( function() {
resetMessage(form);
return false;
} )
)
.append( $("<p></p>") );
var container = $("<div />").appendTo(msgArea)[0];
$("<a />").text("Try again?").click(function() {
$("input", form)[0].focus();
}).appendTo(container);
return msgArea;
}
/*
* Display a message aligned with the provided form.
*/
function displayMessage(form, msg, error, options) {
options = options || {};
if(options.hideForm) {
$(".inputArea", form).hide();
} else {
$(".inputArea", form).show();
}
var msgArea = $(".messageArea", form);
if(msgArea.length === 0) {
msgArea = buildMsgArea(form);
msgArea.prependTo(form);
}
// replace error msg
msgArea
.find("p")
.empty()
.html(msg || ts.locale.error)
.end()
.show(100);
var errorDiv = msgArea.find("div");
if(error) {
msgArea.addClass("error annotation");
errorDiv.show();
} else {
msgArea.removeClass("error");
errorDiv.hide();
}
if(options.annotate) {
$(options.annotate, form).addClass("annotation");
}
}
/*
* Do the default initialization behaviors.
*/
function defaultInit(ts, status, callback, options) {
options = options || {};
if (status.space && status.space.name &&
typeof options.space === "undefined") {
options.space = status.space.name;
}
ts.resolveCurrentSpaceName(options, status.server_host.host);
if(!ts.currentSpace) {
$(document.body).addClass("ts-unknown-space");
}
ts.loadStatus(status);
if(status.identity || ts.parameters.openid) {
ts.register_openid(status.identity);
} else if(status.username && ts.parameters.openid) {
// open id login occurred so redirect to homespace
window.location.href = ts.parameters.redirect ||
ts.getHost(status.username);
}
// do login status
ts.forms.password($("form.ts-password")[0]);
ts.loginStatus();
if(ts.currentSpace) {
ts.initForSpace(status);
}
if(callback) {
callback(ts);
}
}
/*
* Parse query parameters into a simple object.
*/
function parseParameters(queryString) {
var args = queryString.split(/[&;]/),
parameters = {},
i,
nameval;
for(i = 0; i < args.length; i += 1) {
nameval = args[i].split("=");
if(nameval.length === 2) {
parameters[nameval[0]] = nameval[1];
}
}
return parameters;
}
/*
* add CSRF form fields to the provided form.
*/
function addCSRF(form) {
$("<input type='hidden' name='csrf_token' />").
val(getCSRFToken()).appendTo(form);
}
var ts = {
currentSpace: false,
locale: {
error: "An error occurred",
tryAgain: "Please try again",
success: "Clear this notification",
badLogin: "We are unable to log you in with those details.",
charError: "Username is invalid - must only contain lowercase " +
"letters, digits or hyphens",
spaceSuccess: "Successfully created space.",
userError: "Username is already taken, please choose another.",
passwordError: "Passwords do not match",
passwordLengthError: "Error: password must be at least 6 characters",
invalidSpaceError: ["error: invalid space name - must start with a ",
"letter, be at least two characters in length and only contain ",
"lowercase letters, digits or hyphens"].join("")
},
status: {},
user: {},
resolveCurrentSpaceName: function(options, host) {
if(options && typeof options.space !== "undefined") {
ts.currentSpace = options.space;
} else if(window.location.protocol !== "file:") {
var hostname = window.location.hostname;
if(host.split(".").length < hostname.split(".").length) {
ts.currentSpace = hostname.split(".")[0];
}
}
},
isValidSpaceName: function(name) {
return name.match(/^[a-z][0-9a-z\-]*[0-9a-z]$/) ? true : false;
},
init: function(callback, options) {
ts.parameters = parseParameters(window.location.search.substr(1));
var status = tiddlyweb.status;
if (status) {
defaultInit(ts, status, callback, options);
}
},
initForSpace: function(status) {
if (/_private$/.test(status.space.recipe)) {
$(document.body).addClass("ts-member");
ts.forms.addInclude($("form.ts-includes")[0]);
ts.forms.addMember($("form.ts-members")[0]);
ts.forms.addSpace($("form.ts-spaces")[0]);
} else {
$(document.body).addClass("ts-nonmember");
}
ts.initLists();
},
getSpaces: function(callback) {
if(ts.spaces) {
callback(ts.spaces);
} else {
$.ajax({
url: "/spaces?mine=1",
dataType: "json",
success: function(spaces) {
ts.spaces = spaces;
callback(ts.spaces);
},
error: function() {
ts.spaces = false;
}
});
}
},
initLists: function() {
ts.lists.identities();
ts.lists.members();
ts.lists.includes();
},
loadStatus: function(status) {
ts.status = status;
ts.user = {
name: status.username,
anon: status.username ? status.username === "GUEST" : true
};
},
getHost: function(subdomain) {
var s = ts.status;
var host = s.server_host;
subdomain = subdomain ? subdomain + "." : "";
var url = host.scheme + "://" + subdomain + host.host;
var port = host.port;
if(port && port !== "80" && port !== "443") {
url += ":" + port;
}
return url;
},
login: function(username, password, options) {
options = options || {};
var success = options.success || function() {
window.location = options.redirect || ts.getHost(username);
};
// XXX void errback?
var errback = options.errback || function() {};
var challenger = options.challenger = options.challenger ||
"/challenge/tiddlywebplugins.tiddlyspace.cookie_form";
$.ajax({
url: challenger,
type: "POST",
data: {
user: username,
password: password,
csrf_token: getCSRFToken(),
// workaround to marginalize automatic subsequent GET
tiddlyweb_redirect: "/status"
},
success: success,
error: errback
});
},
parameters: {},
register_openid_for_user: function(username, openid) {
var user = new tiddlyweb.User(username, null, "/");
user.identities().add(openid, function() {
window.location.href = window.location.pathname;
}, function() {
throw "failed to add identity to current user.";
});
},
register_openid: function(openid) {
var space = ts.parameters.space;
var username = ts.parameters.user;
if(!space && !username) {
var answer = confirm(
"Would you like to create a space with your openid: " +
openid + "?"
);
if(answer) {
space = prompt("What would you like to be your TiddlySpace username?");
}
}
if(space && openid) {
var possible =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var password = "";
while(password.length < 16) {
password += possible.charAt(Math.floor(Math.random() * possible.length));
}
// register user with username of space and random password
ts.register(space, password, null, {
errback: function() {
throw "failed at step 1/3";
},
success: function() {
// login as that newly created user
ts.login(space, password, {
success: function() {
ts.register_openid_for_user(space, openid);
},
errback: function() {
throw "failed at step 2/3";
}
});
}
});
} else if(username) {
ts.register_openid_for_user(username, ts.parameters.openid);
}
},
register: function(username, password, form, options) {
options = options || {};
var spaceCallback = options.success || function() {
displayMessage(form, ts.locale.spaceSuccess, false);
window.location = options.redirect || ts.getHost(username);
};
var spaceErrback = function (xhr) {
// XXX: 409 unlikely to occur at this point
var msg = xhr.status === 409 ? ts.locale.userError : false;
displayMessage(form, msg, true, options);
};
var userCallback = function() {
ts.login(username, password, {
success: function() {
var space = new tiddlyweb.Space(username, "/");
space.create(spaceCallback, spaceErrback);
}
});
};
var userErrback = function(xhr) {
var msg = xhr.status === 409 ? ts.locale.userError : false;
displayMessage(form, msg, true, options);
};
var user = new tiddlyweb.User(username, password, "/");
user.create(userCallback, userErrback);
},
createSpace: function(form, spaceName, callback, errback) {
if(ts.isValidSpaceName(spaceName)) {
var space = new tiddlyweb.Space(spaceName, "/");
space.create(callback, errback);
} else {
displayMessage(form, ts.locale.invalidSpaceError, true);
}
},
changePassword: function(username, password, npassword, form) {
var pwCallback = function() {
var msg = "Successfully changed your password.";
displayMessage(form, msg);
};
var pwErrback = function() {
var msg = "Old password is incorrect.";
displayMessage(form, msg, true);
};
var user = new tiddlyweb.User(username, password, "/");
user.setPassword(npassword, pwCallback, pwErrback);
},
loginStatus: function() {
var register = $("form.ts-registration");
var login = $("form.ts-login");
var logout = $(".ts-logout");
var user = ts.user;
$("form.ts-openid").each(function(i, el) {
ts.forms.openid(el, { user: user });
});
if(!user.anon) {
$(document.body).addClass("ts-loggedin");
$([register, login]).remove();
logout.each(function(i, el) {
ts.forms.logout(el);
});
} else {
if(register) {
ts.forms.register(register);
}
if(login) {
ts.forms.login(login);
}
logout.remove();
}
}
};
ts.forms = {
password: function(form) {
$(form).submit(function(ev) {
ev.preventDefault();
var oldPass = $("[name=password]").val();
var newPass = $("[name=new_password]").val();
var newPass2 = $("[name=new_password_confirm]").val();
if(newPass !== newPass2) {
var msg = "Passwords do not match";
displayMessage(form, msg, true);
} else if(newPass.length < 6) {
displayMessage(form, ts.locale.passwordLengthError, true);
} else {
ts.changePassword(ts.user.name, oldPass, newPass, form);
}
return false;
});
},
addInclude: function(form) {
if(!form) {
return;
}
addCSRF(form);
$(form).submit(function(ev) {
ev.preventDefault();
var input = $("input[name=spacename]", form);
var space = input.val();
var callback = function() {
ts.lists.includes($("ul.ts-includes").empty()[0]);
input.val("");
var msg = space + " included";
displayMessage(form, msg, false);
};
var errback = function() {
var msg = "Unable to include space with that name.";
displayMessage(form, msg, true);
};
new tiddlyweb.Space(ts.currentSpace, "/").includes().
add(space, callback, errback);
});
},
addMember: function(form) {
if(!form) {
return;
}
addCSRF(form);
$(form).submit(function(ev) {
ev.preventDefault();
var input = $("input[name=username]", form);
var username = input.val();
var spaceName = /^@/.test(username) ? username.slice(1) :
null;
var callback = function() {
ts.lists.members($("ul.ts-members").empty()[0]);
input.val("");
resetMessage(form);
};
var errback = function(xhr) {
if(xhr.status === 403) {
displayMessage(form,
"Unable to add members from a space you " +
"are not a member of",
true);
} else if (xhr.status === 409) {
displayMessage(form,
"Unknown username entered.",
true);
} else {
var msg = "Unknown error occurred.";
displayMessage(form, msg, true);
}
};
if (!spaceName) {
new tiddlyweb.Space(ts.currentSpace, "/").members().
add(username, callback, errback);
} else {
addMembersfromSpace(ts, spaceName, callback, errback);
}
});
},
addSpace: function(form) {
if(!form) {
return;
}
var selector = "[name=spacename]";
addCSRF(form);
$(form).submit(function(ev) {
ev.preventDefault();
var spaceName = $(selector, form).val() || "";
var callback = function() {
var host = ts.getHost(spaceName),
msg = "Successfully created <a href='" +
host + "'>" + host + "</a>.";
displayMessage(form, msg, false);
};
var errback = function() {
var msg = "Problem creating a space with that name.";
displayMessage(form, msg, true);
};
ts.createSpace(form, spaceName, callback, errback);
});
},
register: function(form, options) {
options = options || {};
addCSRF(form);
$(form).submit(function(ev) {
ev.preventDefault();
var username = $("[name=username]", form).val();
var password = $("[name=password]", form).val();
options.redirect = $("[name=redirect]", form).val();
var passwordConfirm = $("[name=password_confirm]",
form).val();
var validName = ts.isValidSpaceName(username);
var validLength = password.length >= 6;
if(validName && validLength && password &&
password === passwordConfirm) {
ts.register(username, password, ev.target, options);
} else {
var msg = validName ?
(!validLength ? ts.locale.passwordLengthError :
ts.locale.passwordError) :
ts.locale.charError;
options.annotate = validName ? "[type=password]" :
"[name=username]";
displayMessage(form, msg, true, options);
}
return false;
});
},
openid: function(form, options) {
addCSRF(form);
$(form).attr("method", "post").
attr("action",
"/challenge/tiddlywebplugins.tiddlyspace.openid").
submit(function(ev) {
var identity = $("input[name=openid]", form).val(),
space = $("input[name=space]", form).val(),
user = options && options.user ?
options.user.name : null;
if(!identity) {
ev.preventDefault();
return displayMessage(form,
"Please provide an openid!");
}
var querystring = "?openid=" + identity;
if(space) {
querystring += "&space=" + space;
}
if(user) {
querystring += "&user=" + user;
}
var redirect = $("[name=redirect]", form).val();
if(redirect) {
querystring += "&redirect=" + redirect;
}
// IMPORTANT: #auth:OpenID=<openid> is read by the openid tiddlyweb plugin
// when present it keeps you logged in as your cookie username
$("<input name='tiddlyweb_redirect' type='hidden' />").
val(window.location.pathname + querystring +
"#auth:OpenID=" + identity).appendTo(form);
});
},
logout: function(form_or_container) {
if(!form_or_container) {
return;
}
var tag = form_or_container.nodeName;
var form;
var isContainer = tag !== "FORM";
if(isContainer) {
var uri = ts.getHost(ts.user.name);
var link = $("<a />").attr({"href": uri,
"target": "_parent"}).text(ts.user.name)[0];
var msg = $("<span class='message' />").
text("Welcome back ").prependTo(form_or_container)[0];
$(msg).append(link);
$("<span />").text("!").appendTo(msg);
form = $("form", form_or_container)[0];
if(!form) {
form = $("<form />").appendTo(form_or_container)[0];
$("<input type='submit' class='button' value='Log out'>").appendTo(form);
}
} else {
form = form_or_container;
}
$(form).attr("action", "/logout").attr("method", "post");
addCSRF(form);
},
login: function(form) {
// do login
addCSRF(form);
var options = {
errback: function(xhr) {
var code = xhr.status;
if(code === 401) {
displayMessage(form, ts.locale.badlogin, true);
} else {
displayMessage(form, ts.locale.tryAgain, true);
}
}
};
function doLogin(ev) {
var user = $("input[name=username]", form).val();
var pass = $("input[name=password]", form).val();
if(!user) {
return displayMessage(form, "Please provide a username!");
}
if(!pass) {
return displayMessage(form, "Please provide a password!");
}
options.redirect = $("input[name=redirect]", form).val();
options.challenger = $(form).attr("action");
ts.login(user, pass, options);
ev.preventDefault();
}
$(form).submit(function(ev) {
doLogin(ev);
});
$(form).keypress(function(ev) {
if(ev.keyCode === 13) {
doLogin(ev);
}
});
}
};
ts.lists = {
identities: function() {
var list = $("ul.ts-identities")[0];
if (list) {
$(list).addClass("ts-loading");
var user = new tiddlyweb.User(ts.user.name, null, "/");
user.identities().get(
function(identities) {
var i;
$(list).removeClass("ts-loading").empty();
for(i = 0; i < identities.length; i += 1) {
$("<li />").text(identities[i]).appendTo(list);
}
},
function() {
$(list).removeClass("ts-loading").empty();
}
);
}
},
includes: function() {
var space = new tiddlyweb.Space(ts.currentSpace, "/");
var removeInclusion = function(ev) {
var item = $(ev.target).parents("li")[0];
var target_space = $(ev.target).data("inclusion");
var callback = function() {
$(item).hide(200);
};
var errback = function() {};
space.includes().remove(target_space, callback, errback);
};
var list = $("ul.ts-includes").addClass("ts-loading")[0];
if(list) {
var callback = function(inclusions) {
var i, item;
$(list).removeClass("ts-loading").empty();
for(i = 0; i < inclusions.length; i += 1) {
item = $("<li />").appendTo(list)[0];
$("<a />")
.text(inclusions[i])
.attr("href", ts.getHost(inclusions[i]))
.attr("title", "visit icluded space")
.appendTo(item);
addDeleteBtn("inclusion", inclusions[i], item, removeInclusion);
}
};
var errback = function(xhr) {
$(list).removeClass("ts-loading").empty();
$("<li class='annotation' />").
text("Error requesting inclusions:" + xhr.status + " " + xhr.statusText).
prependTo(list);
};
space.includes().get(callback, errback);
}
},
members: function() {
var space = new tiddlyweb.Space(ts.currentSpace, "/");
var removeMember = function(ev) {
var list = $(ev).parents("ul.members")[0];
var item = $(ev.target).parents("li")[0];
var member = $(ev.target).data("member");
var callback = function() {
$(item).hide(200, function() {
$(item).remove();
if($("ul.ts-members li:visible").length > 1) {
$("button.delete", list).show();
}
});
$("button.delete", list).hide();
};
var errback = function() {
// Um ought to be something here
};
space.members().remove(member, callback, errback);
};
var list = $("ul.ts-members").addClass("ts-loading")[0];
if(list) {
var callback = function(members) {
var i, item;
$(list).removeClass("ts-loading").empty();
members = members.sort();
for(i = 0; i < members.length; i += 1) {
item = $("<li />").appendTo(list)[0];
$("<a />")
.text(members[i])
.attr("href", ts.getHost(members[i]))
.attr("title", "visit member's home space")
.appendTo(item);
if(members.length > 1) {
addDeleteBtn("member", members[i], item, removeMember);
}
}
};
var errback = function() {
$(list).removeClass("ts-loading").empty();
$("<li class='annotation' />").
text("Only members can see other members.").
prependTo(list);
};
space.members().get(callback, errback);
}
}
};
function addDeleteBtn(type, data, item, cb) {
$("<button />")
.addClass("delete")
.data(type, data)
.attr("title", "remove " + type)
.html("×")
.click(cb)
.appendTo(item);
}
ts.parseParameters = parseParameters;
window.ts = {
init: ts.init
};
}(jQuery));
//}}}