diff --git a/package.json b/package.json index 49d1d4b8..78a0a026 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "Calvin Montgomery", "name": "CyTube", "description": "Online media synchronizer and chat", - "version": "3.24.3", + "version": "3.25.0", "repository": { "url": "http://github.com/calzoneman/sync" }, diff --git a/src/channel/opts.js b/src/channel/opts.js index 175fd28b..c4d33612 100644 --- a/src/channel/opts.js +++ b/src/channel/opts.js @@ -3,6 +3,10 @@ var Config = require("../config"); var Utilities = require("../utilities"); var url = require("url"); +function realTypeOf(thing) { + return thing === null ? 'null' : typeof thing; +} + function OptionsModule(channel) { ChannelModule.apply(this, arguments); this.opts = { @@ -100,8 +104,11 @@ OptionsModule.prototype.handleSetOptions = function (user, data) { return; } + var sendUpdate = false; + if ("allow_voteskip" in data) { this.opts.allow_voteskip = Boolean(data.allow_voteskip); + sendUpdate = true; } if ("voteskip_ratio" in data) { @@ -110,6 +117,7 @@ OptionsModule.prototype.handleSetOptions = function (user, data) { ratio = 0; } this.opts.voteskip_ratio = ratio; + sendUpdate = true; } if ("afk_timeout" in data) { @@ -125,12 +133,14 @@ OptionsModule.prototype.handleSetOptions = function (user, data) { u.autoAFK(); }); } + sendUpdate = true; } if ("pagetitle" in data && user.account.effectiveRank >= 3) { var title = (""+data.pagetitle).substring(0, 100); if (!title.trim().match(Config.get("reserved-names.pagetitles"))) { this.opts.pagetitle = (""+data.pagetitle).substring(0, 100); + sendUpdate = true; } else { user.socket.emit("errorMsg", { msg: "That pagetitle is reserved", @@ -151,63 +161,90 @@ OptionsModule.prototype.handleSetOptions = function (user, data) { ml = 0; } this.opts.maxlength = ml; + sendUpdate = true; } if ("externalcss" in data && user.account.effectiveRank >= 3) { - var link = (""+data.externalcss).substring(0, 255); - if (!link) { - this.opts.externalcss = ""; - } else { - try { - var data = url.parse(link); - if (!data.protocol || !data.protocol.match(/^(https?|ftp):/)) { - throw "Unacceptable protocol " + data.protocol; - } else if (!data.host) { - throw "URL is missing host"; - } else { - link = data.href; - } - } catch (e) { - user.socket.emit("errorMsg", { - msg: "Invalid URL for external CSS: " + e, - alert: true - }); - return; - } + var prefix = "Invalid URL for external CSS: "; + if (typeof data.externalcss !== "string") { + user.socket.emit("validationError", { + target: "#cs-externalcss", + message: prefix + "URL must be a string, not " + + realTypeOf(data.externalcss) + }); + } - this.opts.externalcss = link; + var link = data.externalcss.substring(0, 255).trim(); + if (!link) { + sendUpdate = (this.opts.externalcss !== ""); + this.opts.externalcss = ""; + user.socket.emit("validationPassed", { + target: "#cs-externalcss" + }); + } else { + var data = url.parse(link); + if (!data.protocol || data.protocol !== 'https:') { + user.socket.emit("validationError", { + target: "#cs-externalcss", + message: prefix + " URL must begin with 'https://'" + }); + } else if (!data.host) { + user.socket.emit("validationError", { + target: "#cs-externalcss", + message: prefix + "missing hostname" + }); + } else { + user.socket.emit("validationPassed", { + target: "#cs-externalcss" + }); + this.opts.externalcss = data.href; + sendUpdate = true; + } } } if ("externaljs" in data && user.account.effectiveRank >= 3) { - var link = (""+data.externaljs).substring(0, 255); + var prefix = "Invalid URL for external JS: "; + if (typeof data.externaljs !== "string") { + user.socket.emit("validationError", { + target: "#cs-externaljs", + message: prefix + "URL must be a string, not " + + realTypeOf(data.externaljs) + }); + } + + var link = data.externaljs.substring(0, 255).trim(); if (!link) { + sendUpdate = (this.opts.externaljs !== ""); this.opts.externaljs = ""; + user.socket.emit("validationPassed", { + target: "#cs-externaljs" + }); } else { - - try { - var data = url.parse(link); - if (!data.protocol || !data.protocol.match(/^(https?|ftp):/)) { - throw "Unacceptable protocol " + data.protocol; - } else if (!data.host) { - throw "URL is missing host"; - } else { - link = data.href; - } - } catch (e) { - user.socket.emit("errorMsg", { - msg: "Invalid URL for external JS: " + e, - alert: true + var data = url.parse(link); + if (!data.protocol || data.protocol !== 'https:') { + user.socket.emit("validationError", { + target: "#cs-externaljs", + message: prefix + " URL must begin with 'https://'" }); - return; + } else if (!data.host) { + user.socket.emit("validationError", { + target: "#cs-externaljs", + message: prefix + "missing hostname" + }); + } else { + user.socket.emit("validationPassed", { + target: "#cs-externaljs" + }); + this.opts.externaljs = data.href; + sendUpdate = true; } - - this.opts.externaljs = link; } } if ("chat_antiflood" in data) { this.opts.chat_antiflood = Boolean(data.chat_antiflood); + sendUpdate = true; } if ("chat_antiflood_params" in data) { @@ -238,38 +275,46 @@ OptionsModule.prototype.handleSetOptions = function (user, data) { sustained: s, cooldown: c }; + sendUpdate = true; } if ("show_public" in data && user.account.effectiveRank >= 3) { this.opts.show_public = Boolean(data.show_public); + sendUpdate = true; } if ("enable_link_regex" in data) { this.opts.enable_link_regex = Boolean(data.enable_link_regex); + sendUpdate = true; } if ("password" in data && user.account.effectiveRank >= 3) { var pw = data.password + ""; pw = pw === "" ? false : pw.substring(0, 100); this.opts.password = pw; + sendUpdate = true; } if ("allow_dupes" in data) { this.opts.allow_dupes = Boolean(data.allow_dupes); + sendUpdate = true; } if ("torbanned" in data && user.account.effectiveRank >= 3) { this.opts.torbanned = Boolean(data.torbanned); + sendUpdate = true; } if ("allow_ascii_control" in data && user.account.effectiveRank >= 3) { this.opts.allow_ascii_control = Boolean(data.allow_ascii_control); + sendUpdate = true; } if ("playlist_max_per_user" in data && user.account.effectiveRank >= 3) { var max = parseInt(data.playlist_max_per_user); if (!isNaN(max) && max >= 0) { this.opts.playlist_max_per_user = max; + sendUpdate = true; } } @@ -277,6 +322,7 @@ OptionsModule.prototype.handleSetOptions = function (user, data) { var delay = data.new_user_chat_delay; if (!isNaN(delay) && delay >= 0) { this.opts.new_user_chat_delay = delay; + sendUpdate = true; } } @@ -284,11 +330,14 @@ OptionsModule.prototype.handleSetOptions = function (user, data) { var delay = data.new_user_chat_link_delay; if (!isNaN(delay) && delay >= 0) { this.opts.new_user_chat_link_delay = delay; + sendUpdate = true; } } this.channel.logger.log("[mod] " + user.getName() + " updated channel options"); - this.sendOpts(this.channel.users); + if (sendUpdate) { + this.sendOpts(this.channel.users); + } }; module.exports = OptionsModule; diff --git a/src/web/account.js b/src/web/account.js index 2073fbe6..2b0e0479 100644 --- a/src/web/account.js +++ b/src/web/account.js @@ -13,6 +13,7 @@ var Config = require("../config"); var Server = require("../server"); var session = require("../session"); var csrf = require("./csrf"); +const url = require("url"); /** * Handles a GET request for /account/edit @@ -396,6 +397,25 @@ function handleAccountProfilePage(req, res) { }); } +function validateProfileImage(image, callback) { + var prefix = "Invalid URL for profile image: "; + var link = image.trim(); + if (!link) { + process.nextTick(callback, null, link); + } else { + var data = url.parse(link); + if (!data.protocol || data.protocol !== 'https:') { + process.nextTick(callback, + new Error(prefix + " URL must begin with 'https://'")); + } else if (!data.host) { + process.nextTick(callback, + new Error(prefix + "missing hostname")); + } else { + process.nextTick(callback, null, link); + } + } +} + /** * Handles a POST request to edit a profile */ @@ -410,23 +430,37 @@ function handleAccountProfile(req, res) { }); } - var image = req.body.image; - var text = req.body.text; + var rawImage = String(req.body.image).substring(0, 255); + var text = String(req.body.text).substring(0, 255); - db.users.setProfile(req.user.name, { image: image, text: text }, function (err) { - if (err) { - sendPug(res, "account-profile", { - profileImage: "", - profileText: "", - profileError: err + validateProfileImage(rawImage, (error, image) => { + if (error) { + db.users.getProfile(req.user.name, function (err, profile) { + var errorMessage = err || error.message; + sendPug(res, "account-profile", { + profileImage: profile ? profile.image : "", + profileText: profile ? profile.text : "", + profileError: errorMessage + }); }); return; } - sendPug(res, "account-profile", { - profileImage: image, - profileText: text, - profileError: false + db.users.setProfile(req.user.name, { image: image, text: text }, function (err) { + if (err) { + sendPug(res, "account-profile", { + profileImage: "", + profileText: "", + profileError: err + }); + return; + } + + sendPug(res, "account-profile", { + profileImage: image, + profileText: text, + profileError: false + }); }); }); } diff --git a/templates/account-profile.pug b/templates/account-profile.pug index 25ca1574..56faf95d 100644 --- a/templates/account-profile.pug +++ b/templates/account-profile.pug @@ -35,13 +35,42 @@ html(lang="en") input(type="hidden", name="_csrf", value=csrfToken) .form-group label.control-label(for="profileimage") Image - input#profileimage.form-control(type="text", name="image") + input#profileimage.form-control(type="text", name="image", maxlength="255") .form-group label.control-label(for="profiletext") Text - textarea#profiletext.form-control(cols="10", name="text")= profileText + textarea#profiletext.form-control(cols="10", name="text", maxlength="255")= profileText button.btn.btn-primary.btn-block(type="submit") Save include footer +footer() script(type="text/javascript"). - $("#profileimage").val("#{profileImage}"); + var $profileImage = $("#profileimage"); + $profileImage.val("#{profileImage}"); + var hasError = false; + function validateImage() { + var value = $profileImage.val().trim(); + $profileImage.val(value); + if (!/^$|^https:/.test(value)) { + hasError = true; + $profileImage.parent().addClass("has-error"); + var $error = $("#profileimage-error"); + if ($error.length === 0) { + $error = $("
") + .attr({ id: "profileimage-error" }) + .addClass("text-danger") + .html("Profile image must be a URL beginning withhttps://")
+ .insertAfter($profileImage);
+ }
+ } else {
+ hasError = false;
+ $profileImage.parent().removeClass("has-error");
+ $("#profileimage-error").remove();
+ }
+ }
+
+ $("form").submit(function (event) {
+ validateImage();
+ if (hasError) {
+ event.preventDefault();
+ }
+ });
diff --git a/www/js/callbacks.js b/www/js/callbacks.js
index 6b89328e..d9b54c0b 100644
--- a/www/js/callbacks.js
+++ b/www/js/callbacks.js
@@ -49,7 +49,6 @@ Callbacks = {
},
costanza: function (data) {
- hidePlayer();
$("#costanza-modal").modal("hide");
var modal = makeModal();
modal.attr("id", "costanza-modal")
@@ -61,7 +60,6 @@ Callbacks = {
.appendTo(body);
$("").text(data.msg).appendTo(body);
- hidePlayer();
modal.modal();
},
@@ -1052,6 +1050,38 @@ Callbacks = {
HAS_CONNECTED_BEFORE = false;
ioServerConnect(socketConfig);
setupCallbacks();
+ },
+
+ validationError: function (error) {
+ var target = $(error.target);
+ target.parent().find(".text-danger").remove();
+
+ var formGroup = target.parent();
+ while (!formGroup.hasClass("form-group") && formGroup.length > 0) {
+ formGroup = formGroup.parent();
+ }
+
+ if (formGroup.length > 0) {
+ formGroup.addClass("has-error");
+ }
+
+ $("").addClass("text-danger")
+ .text(error.message)
+ .insertAfter(target);
+ },
+
+ validationPassed: function (data) {
+ var target = $(data.target);
+ target.parent().find(".text-danger").remove();
+
+ var formGroup = target.parent();
+ while (!formGroup.hasClass("form-group") && formGroup.length > 0) {
+ formGroup = formGroup.parent();
+ }
+
+ if (formGroup.length > 0) {
+ formGroup.removeClass("has-error");
+ }
}
}
diff --git a/www/js/ui.js b/www/js/ui.js
index 2e6b6869..83c66ae4 100644
--- a/www/js/ui.js
+++ b/www/js/ui.js
@@ -488,7 +488,6 @@ $("#voteskip").click(function() {
$("#getplaylist").click(function() {
var callback = function(data) {
- hidePlayer();
var idx = socket.listeners("errorMsg").indexOf(errCallback);
if (idx >= 0) {
socket.listeners("errorMsg").splice(idx);
@@ -523,7 +522,6 @@ $("#getplaylist").click(function() {
$("").addClass("modal-footer").appendTo(modal);
outer.on("hidden.bs.modal", function() {
outer.remove();
- unhidePlayer();
});
outer.modal();
};
@@ -862,7 +860,6 @@ applyOpts();
})();
var EMOTELISTMODAL = $("#emotelist");
-EMOTELISTMODAL.on("hidden.bs.modal", unhidePlayer);
$("#emotelistbtn").click(function () {
EMOTELISTMODAL.modal();
});
diff --git a/www/js/util.js b/www/js/util.js
index a2f7760d..ef6a41cd 100644
--- a/www/js/util.js
+++ b/www/js/util.js
@@ -616,11 +616,6 @@ function rebuildPlaylist() {
/* user settings menu */
function showUserOptions() {
- hidePlayer();
- $("#useroptions").on("hidden.bs.modal", function () {
- unhidePlayer();
- });
-
if (CLIENT.rank < 2) {
$("a[href='#us-mod']").parent().hide();
} else {
@@ -2046,21 +2041,6 @@ function waitUntilDefined(obj, key, fn) {
fn();
}
-function hidePlayer() {
- /* 2015-09-16
- * Originally used to hide the player while a modal was open because of
- * certain flash videos that always rendered on top. Seems to no longer
- * be an issue. Uncomment this if it is.
- if (!PLAYER) return;
-
- $("#ytapiplayer").hide();
- */
-}
-
-function unhidePlayer() {
- //$("#ytapiplayer").show();
-}
-
function chatDialog(div) {
var parent = $("").addClass("profile-box")
.css({
@@ -2103,6 +2083,44 @@ function errDialog(err) {
return div;
}
+/**
+ * 2016-12-08
+ * I *promise* that one day I will actually split this file into submodules
+ * -cal
+ */
+function modalAlert(options) {
+ if (typeof options !== "object" || options === null) {
+ throw new Error("modalAlert() called without required parameter");
+ }
+
+ var modal = makeModal();
+ modal.addClass("cytube-modal-alert");
+ modal.removeClass("fade");
+ modal.find(".modal-dialog").addClass("modal-dialog-nonfluid");
+
+ if (options.title) {
+ $("").text(options.title).appendTo(modal.find(".modal-header"));
+ }
+
+ var contentDiv = $("").addClass("modal-body");
+ if (options.htmlContent) {
+ contentDiv.html(options.htmlContent);
+ } else if (options.textContent) {
+ contentDiv.text(options.textContent);
+ }
+
+ contentDiv.appendTo(modal.find(".modal-content"));
+
+ var footer = $("").addClass("modal-footer");
+ var okButton = $("").addClass("btn btn-primary")
+ .attr({ "data-dismiss": "modal"})
+ .text("OK")
+ .appendTo(footer);
+ footer.appendTo(modal.find(".modal-content"));
+ modal.appendTo(document.body);
+ modal.modal();
+}
+
function queueMessage(data, type) {
if (!data)
data = { link: null };
@@ -2237,7 +2255,6 @@ function makeModal() {
.appendTo(head);
wrap.on("hidden.bs.modal", function () {
- unhidePlayer();
wrap.remove();
});
return wrap;
@@ -3160,11 +3177,6 @@ window.CSEMOTELIST = new CSEmoteList("#cs-emotes");
window.CSEMOTELIST.sortAlphabetical = USEROPTS.emotelist_sort;
function showChannelSettings() {
- hidePlayer();
- $("#channeloptions").on("hidden.bs.modal", function () {
- unhidePlayer();
- });
-
$("#channeloptions").modal();
}