Compare commits

..

40 commits

Author SHA1 Message Date
Calvin Montgomery b9f66a8580 Fix update mediaquery git hash 2022-09-18 19:10:09 -07:00
Calvin Montgomery 9d9f638496 Tweaks 2022-09-18 16:23:30 -07:00
Xaekai 0fe2d9afb9 Update custom manifest documentation regarding audioTracks 2022-09-04 14:13:42 -07:00
Xaekai f7972af085 Add audioTracks support for custom manifests 2022-09-04 14:13:34 -07:00
Xaekai ff47583e06 Add disposal to audio switcher 2022-09-04 14:09:36 -07:00
Xaekai 0e410e4f2d Add compiled JSO libraries 2022-09-04 14:09:36 -07:00
Xaekai e713eca9cc Track last chatMsg time, and ignore reconnect spam 2022-09-04 14:09:36 -07:00
Xaekai 5ecca27c9f Add vjs plugin for audio track switching 2022-09-04 14:09:36 -07:00
Xaekai 568b7f3cfe Move add to be first playlist control 2022-09-04 14:09:36 -07:00
Xaekai ef0a04ddcb Eliminate jQuery in index template microscript 2022-09-04 14:09:36 -07:00
Xaekai 0226d73272 Focus searchbox when emotelist modal is shown 2022-09-04 14:09:36 -07:00
Xaekai 24fdc3639a Fix Nicovideo methods 2022-09-04 14:09:35 -07:00
Xaekai 9bd8fee92f Update vjs components
Upgrade Video.js core to v7.18.0 from v5.10.7
Upgrade Dash.js to v4.2.8 from v2.6.3
Upgrade videojs-contrib-dash to v5.1.1 from v2.9.1
Modify videojs-resolution-switcher
2022-09-04 14:09:35 -07:00
Xaekai e2944e0769 Move Video.js components to a subfolder 2022-09-04 14:09:35 -07:00
Xaekai 94e2fd10ad Remove all references to wmode
Usage of wmode was specific to Flash, which is long dead.
2022-09-04 14:09:35 -07:00
Xaekai dd051098bc Add Niconico support 2022-09-04 14:09:35 -07:00
Xaekai 094c9a7c4e Update HLS support 2022-09-04 14:09:35 -07:00
Xaekai 85118db86e Reorganize PlayerJSPlayer dependents 2022-09-04 14:09:35 -07:00
Xaekai 6b39d754d3 Add Odysee support 2022-09-04 14:09:35 -07:00
Xaekai 89d79fe422 Set videojs poster on player ready
Resolves Github issue #870
2022-09-04 14:09:35 -07:00
Xaekai 8da1d1c772 Add BandCamp support 2022-09-04 14:09:27 -07:00
Xaekai c506ccf05b Enable caching BitChute metadata 2022-09-04 14:02:09 -07:00
Xaekai 8dde08ac6d Use child iframe for BitChute
By using an iframe we can take advantage of the referrer meta tag,
while still being able to scaffold everything relatively easily because it's same-origin
2022-09-04 14:02:09 -07:00
Xaekai 6a0119fa17 Flash is long dead 2022-09-04 14:02:09 -07:00
Xaekai b7188832da Options to autoembed PeerTube 2022-09-04 14:02:09 -07:00
Xaekai a92df0c6d9 Touch up data.js
Reorder useropts to match client
Remove long unused variable
2022-09-04 14:02:09 -07:00
Xaekai 9a639654f4 Fix issue with queue progress
If the user queues a PeerTube link with a long uuid the progress bar would never go away. Now it will just check against the hostname.
2022-09-04 14:02:09 -07:00
Xaekai 6812884760 Fixup Livestream.com 2022-09-04 14:02:09 -07:00
Xaekai d3aed7121b Add BitChute support 2022-09-04 14:02:09 -07:00
Xaekai eb71718bea Fixup various lint
Touched up callbacks and paginator
2022-09-04 14:02:09 -07:00
Xaekai 5e1cfc41d9 Eliminate jQuery from inline js/css charlimit notice 2022-09-04 14:02:09 -07:00
Xaekai 67b61d69dc Eliminate jQuery event shorthands 2022-09-04 14:01:49 -07:00
Xaekai cfe009323f Improve the ESLint situation 2022-09-04 13:58:42 -07:00
Xaekai 3212aa5df6 Allow for the omission of particular frames in SOCKET_DEBUG
In particular, mediaUpdate spam.
2022-09-04 13:58:42 -07:00
Xaekai af6e7bed0c EmoteList live reconfig support 2022-09-04 13:58:42 -07:00
Xaekai 46fcec8d14 Update jQuery and jQuery UI 2022-09-04 13:58:42 -07:00
Xaekai 614a039266 Add PeerTube support 2022-09-04 13:58:42 -07:00
Xaekai f365d4192a Refactor parseMediaLink 2022-09-04 13:58:42 -07:00
Xaekai e1c7b76650 Remove references to defunct services
Imgur discontinued support for albums
SmashCast/Hitbox disappeared
Ustream was sunset by IBM
Mixer is dead
Picasa is long dead
Vidme is long dead
IE11 is dead
2022-09-04 13:58:42 -07:00
Xaekai 34e2130ce6 Ignore patch files 2022-09-04 13:58:42 -07:00
33 changed files with 8462 additions and 3479 deletions

View file

@ -1,6 +0,0 @@
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_size = 4
indent_style = space

21
NEWS.md
View file

@ -1,24 +1,3 @@
2022-09-21
==========
**Upgrade intervention required**
This release adds a feature to ban channels, replacing the earlier (hastily
added) configuration-based `channel-blacklist`. If you have any entries in
`channel-blacklist` in your `config.yaml`, you will need to migrate them to the
new bans table by using a command after upgrading (the ACP web interface hasn't
been updated for this feature):
./bin/admin.js ban-channel <channel-name> <external-reason> <internal-reason>
The external reason will be displayed when users attempt to join the banned
channel, while the internal reason is only displayed when using the
`show-channel-ban` command.
You can later use `unban-channel` to remove a ban. The owner of the banned
channel can still delete it, but the banned state will persist, so the channel
cannot be re-registered later.
2022-08-28 2022-08-28
========== ==========

View file

@ -1,176 +0,0 @@
#!/usr/bin/env node
const Config = require('../lib/config');
Config.load('config.yaml');
if (!Config.get('service-socket.enabled')){
console.error('The Service Socket is not enabled.');
process.exit(1);
}
const net = require('net');
const path = require('path');
const readline = require('node:readline/promises');
const socketPath = path.resolve(__dirname, '..', Config.get('service-socket.socket'));
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
async function doCommand(params) {
return new Promise((resolve, reject) => {
const client = net.createConnection(socketPath);
client.on('connect', () => {
client.write(JSON.stringify(params) + '\n');
});
client.on('data', data => {
client.end();
resolve(JSON.parse(data));
});
client.on('error', error => {
reject(error);
});
});
}
let commands = [
{
command: 'ban-channel',
handler: async args => {
if (args.length !== 3) {
console.log('Usage: ban-channel <name> <externalReason> <internalReason>');
process.exit(1);
}
let [name, externalReason, internalReason] = args;
let answer = await rl.question(`Ban ${name} with external reason "${externalReason}" and internal reason "${internalReason}"? `);
if (!/^[yY]$/.test(answer)) {
console.log('Aborted.');
process.exit(1);
}
let res = await doCommand({
command: 'ban-channel',
name,
externalReason,
internalReason
});
switch (res.status) {
case 'error':
console.log('Error:', res.error);
process.exit(1);
break;
case 'success':
console.log('Ban succeeded.');
process.exit(0);
break;
default:
console.log(`Unknown result: ${res.status}`);
process.exit(1);
break;
}
}
},
{
command: 'unban-channel',
handler: async args => {
if (args.length !== 1) {
console.log('Usage: unban-channel <name>');
process.exit(1);
}
let [name] = args;
let answer = await rl.question(`Unban ${name}? `);
if (!/^[yY]$/.test(answer)) {
console.log('Aborted.');
process.exit(1);
}
let res = await doCommand({
command: 'unban-channel',
name
});
switch (res.status) {
case 'error':
console.log('Error:', res.error);
process.exit(1);
break;
case 'success':
console.log('Unban succeeded.');
process.exit(0);
break;
default:
console.log(`Unknown result: ${res.status}`);
process.exit(1);
break;
}
}
},
{
command: 'show-banned-channel',
handler: async args => {
if (args.length !== 1) {
console.log('Usage: show-banned-channel <name>');
process.exit(1);
}
let [name] = args;
let res = await doCommand({
command: 'show-banned-channel',
name
});
switch (res.status) {
case 'error':
console.log('Error:', res.error);
process.exit(1);
break;
case 'success':
if (res.ban != null) {
console.log(`Channel: ${name}`);
console.log(`Ban issued: ${res.ban.createdAt}`);
console.log(`Banned by: ${res.ban.bannedBy}`);
console.log(`External reason:\n${res.ban.externalReason}`);
console.log(`Internal reason:\n${res.ban.internalReason}`);
} else {
console.log(`Channel ${name} is not banned.`);
}
process.exit(0);
break;
default:
console.log(`Unknown result: ${res.status}`);
process.exit(1);
break;
}
}
}
];
let found = false;
commands.forEach(cmd => {
if (cmd.command === process.argv[2]) {
found = true;
cmd.handler(process.argv.slice(3)).then(() => {
process.exit(0);
}).catch(error => {
console.log('Error in command:', error.stack);
});
}
});
if (!found) {
console.log('Available commands:');
commands.forEach(cmd => {
console.log(` * ${cmd.command}`);
});
process.exit(1);
}

View file

@ -128,8 +128,6 @@ max-channels-per-user: 5
max-accounts-per-ip: 5 max-accounts-per-ip: 5
# Minimum number of seconds between guest logins from the same IP # Minimum number of seconds between guest logins from the same IP
guest-login-delay: 60 guest-login-delay: 60
# Maximum character length of a chat message, capped at 1000 characters
max-chat-message-length: 320
# Allows you to customize the path divider. The /r/ in http://localhost/r/yourchannel # Allows you to customize the path divider. The /r/ in http://localhost/r/yourchannel
# Acceptable characters are a-z A-Z 0-9 _ and - # Acceptable characters are a-z A-Z 0-9 _ and -

View file

@ -110,25 +110,6 @@ describe('KickbanModule', () => {
); );
}); });
it('rejects if the username is invalid', done => {
mockUser.socket.emit = (frame, obj) => {
if (frame === 'errorMsg') {
assert.strictEqual(
obj.msg,
'Invalid username'
);
done();
}
};
kickban.handleCmdBan(
mockUser,
'/ban test_user<>%$# because reasons',
{}
);
});
it('rejects if the user does not have ban permission', done => { it('rejects if the user does not have ban permission', done => {
mockUser.socket.emit = (frame, obj) => { mockUser.socket.emit = (frame, obj) => {
if (frame === 'errorMsg') { if (frame === 'errorMsg') {

View file

@ -1,109 +0,0 @@
const assert = require('assert');
const { BannedChannelsController } = require('../../lib/controller/banned-channels');
const dbChannels = require('../../lib/database/channels');
const testDB = require('../testutil/db').testDB;
const { EventEmitter } = require('events');
require('../../lib/database').init(testDB);
const testBan = {
name: 'ban_test_1',
externalReason: 'because I said so',
internalReason: 'illegal content',
bannedBy: 'admin'
};
async function cleanupTestBan() {
return dbChannels.removeBannedChannel(testBan.name);
}
describe('BannedChannelsController', () => {
let controller;
let messages;
beforeEach(async () => {
await cleanupTestBan();
messages = new EventEmitter();
controller = new BannedChannelsController(
dbChannels,
messages
);
});
afterEach(async () => {
await cleanupTestBan();
});
it('bans a channel', async () => {
assert.strictEqual(await controller.getBannedChannel(testBan.name), null);
let received = null;
messages.once('ChannelBanned', cb => {
received = cb;
});
await controller.banChannel(testBan);
let info = await controller.getBannedChannel(testBan.name);
for (let field of Object.keys(testBan)) {
// Consider renaming parameter to avoid this branch
if (field === 'name') {
assert.strictEqual(info.channelName, testBan.name);
} else {
assert.strictEqual(info[field], testBan[field]);
}
}
assert.notEqual(received, null);
assert.strictEqual(received.channel, testBan.name);
assert.strictEqual(received.externalReason, testBan.externalReason);
});
it('updates an existing ban', async () => {
let received = [];
messages.on('ChannelBanned', cb => {
received.push(cb);
});
await controller.banChannel(testBan);
let testBan2 = { ...testBan, externalReason: 'because of reasons' };
await controller.banChannel(testBan2);
let info = await controller.getBannedChannel(testBan2.name);
for (let field of Object.keys(testBan2)) {
// Consider renaming parameter to avoid this branch
if (field === 'name') {
assert.strictEqual(info.channelName, testBan2.name);
} else {
assert.strictEqual(info[field], testBan2[field]);
}
}
assert.deepStrictEqual(received, [
{
channel: testBan.name,
externalReason: testBan.externalReason
},
{
channel: testBan2.name,
externalReason: testBan2.externalReason
},
]);
});
it('unbans a channel', async () => {
let received = null;
messages.once('ChannelUnbanned', cb => {
received = cb;
});
await controller.banChannel(testBan);
await controller.unbanChannel(testBan.name, testBan.bannedBy);
let info = await controller.getBannedChannel(testBan.name);
assert.strictEqual(info, null);
assert.notEqual(received, null);
assert.strictEqual(received.channel, testBan.name);
});
});

11014
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -2,17 +2,17 @@
"author": "Calvin Montgomery", "author": "Calvin Montgomery",
"name": "CyTube", "name": "CyTube",
"description": "Online media synchronizer and chat", "description": "Online media synchronizer and chat",
"version": "3.86.0", "version": "3.83.0",
"repository": { "repository": {
"url": "http://github.com/calzoneman/sync" "url": "http://github.com/calzoneman/sync"
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@calzoneman/jsli": "^2.0.1", "@calzoneman/jsli": "^2.0.1",
"@cytube/mediaquery": "github:CyTube/mediaquery#564d0c4615e80f72722b0f68ac81f837a4c5fc81", "@cytube/mediaquery": "github:CyTube/mediaquery#c1dcf792cd6e9977c04c1e96f23315dad5e3294d",
"bcrypt": "^5.0.1", "bcrypt": "^5.0.1",
"bluebird": "^3.7.2", "bluebird": "^3.7.2",
"body-parser": "^1.20.1", "body-parser": "^1.19.0",
"cheerio": "^1.0.0-rc.10", "cheerio": "^1.0.0-rc.10",
"clone": "^2.1.2", "clone": "^2.1.2",
"compression": "^1.7.4", "compression": "^1.7.4",
@ -20,22 +20,21 @@
"create-error": "^0.3.1", "create-error": "^0.3.1",
"csrf": "^3.1.0", "csrf": "^3.1.0",
"cytubefilters": "github:calzoneman/cytubefilters#c67b2dab2dc5cc5ed11018819f71273d0f8a1bf5", "cytubefilters": "github:calzoneman/cytubefilters#c67b2dab2dc5cc5ed11018819f71273d0f8a1bf5",
"express": "^4.18.2", "express": "^4.17.1",
"express-minify": "^1.0.0", "express-minify": "^1.0.0",
"json-typecheck": "^0.1.3", "json-typecheck": "^0.1.3",
"knex": "^2.4.0", "knex": "^0.95.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"mysql": "^2.18.1",
"nodemailer": "^6.6.1", "nodemailer": "^6.6.1",
"pg": "^8.11.3",
"pg-native": "^3.0.1",
"prom-client": "^13.1.0", "prom-client": "^13.1.0",
"proxy-addr": "^2.0.6", "proxy-addr": "^2.0.6",
"pug": "^3.0.2", "pug": "^3.0.2",
"redis": "^3.1.1", "redis": "^3.1.1",
"sanitize-html": "^2.7.0", "sanitize-html": "^2.7.0",
"serve-static": "^1.15.0", "serve-static": "^1.14.1",
"socket.io": "^4.5.4", "socket.io": "^4.5.0",
"source-map-support": "^0.5.19", "source-map-support": "^0.5.19",
"toml": "^3.0.0", "toml": "^3.0.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",

View file

@ -15,19 +15,8 @@ window.CustomEmbedPlayer = class CustomEmbedPlayer extends EmbedPlayer
return return
embedSrc = data.meta.embed.src embedSrc = data.meta.embed.src
link = "<a href=\"#{embedSrc}\" target=\"_blank\"><strong>#{embedSrc}</strong></a>"
link = document.createElement('a') alert = makeAlert('Untrusted Content', CUSTOM_EMBED_WARNING.replace('%link%', link),
link.href = embedSrc
link.target = '_blank'
link.rel = 'noopener noreferer'
strong = document.createElement('strong')
strong.textContent = embedSrc
link.appendChild(strong)
# TODO: Ideally makeAlert() would allow optionally providing a DOM
# element instead of requiring HTML text
alert = makeAlert('Untrusted Content', CUSTOM_EMBED_WARNING.replace('%link%', link.outerHTML),
'alert-warning') 'alert-warning')
.removeClass('col-md-12') .removeClass('col-md-12')
$('<button/>').addClass('btn btn-default') $('<button/>').addClass('btn btn-default')

View file

@ -94,10 +94,7 @@ function Channel(name) {
}, USERCOUNT_THROTTLE); }, USERCOUNT_THROTTLE);
const self = this; const self = this;
db.channels.load(this, function (err) { db.channels.load(this, function (err) {
if (err && err.code === 'EBANNED') { if (err && err !== "Channel is not registered") {
self.emit("loadFail", err.message);
self.setFlag(Flags.C_ERROR);
} else if (err && err !== "Channel is not registered") {
self.emit("loadFail", "Failed to load channel data from the database. Please try again later."); self.emit("loadFail", "Failed to load channel data from the database. Please try again later.");
self.setFlag(Flags.C_ERROR); self.setFlag(Flags.C_ERROR);
} else { } else {

View file

@ -162,7 +162,7 @@ ChatModule.prototype.handleChatMsg = function (user, data) {
return; return;
} }
data.msg = data.msg.substring(0, Config.get("max-chat-message-length")); data.msg = data.msg.substring(0, 320);
// Restrict new accounts/IPs from chatting and posting links // Restrict new accounts/IPs from chatting and posting links
if (this.restrictNewAccount(user, data)) { if (this.restrictNewAccount(user, data)) {
@ -248,7 +248,7 @@ ChatModule.prototype.handlePm = function (user, data) {
} }
data.msg = data.msg.substring(0, Config.get("max-chat-message-length")); data.msg = data.msg.substring(0, 320);
var to = null; var to = null;
for (var i = 0; i < this.channel.users.length; i++) { for (var i = 0; i < this.channel.users.length; i++) {
if (this.channel.users[i].getLowerName() === data.to) { if (this.channel.users[i].getLowerName() === data.to) {

View file

@ -4,7 +4,6 @@ var Flags = require("../flags");
var util = require("../utilities"); var util = require("../utilities");
var Account = require("../account"); var Account = require("../account");
import Promise from 'bluebird'; import Promise from 'bluebird';
const XSS = require("../xss");
const dbIsNameBanned = Promise.promisify(db.channels.isNameBanned); const dbIsNameBanned = Promise.promisify(db.channels.isNameBanned);
const dbIsIPBanned = Promise.promisify(db.channels.isIPBanned); const dbIsIPBanned = Promise.promisify(db.channels.isIPBanned);
@ -262,6 +261,7 @@ KickBanModule.prototype.handleCmdIPBan = function (user, msg, _meta) {
chan.refCounter.ref("KickBanModule::handleCmdIPBan"); chan.refCounter.ref("KickBanModule::handleCmdIPBan");
this.banAll(user, name, range, reason).catch(error => { this.banAll(user, name, range, reason).catch(error => {
//console.log('!!!', error.stack);
const message = error.message || error; const message = error.message || error;
user.socket.emit("errorMsg", { msg: message }); user.socket.emit("errorMsg", { msg: message });
}).then(() => { }).then(() => {
@ -276,10 +276,6 @@ KickBanModule.prototype.checkChannelAlive = function checkChannelAlive() {
}; };
KickBanModule.prototype.banName = async function banName(actor, name, reason) { KickBanModule.prototype.banName = async function banName(actor, name, reason) {
if (!util.isValidUserName(name)) {
throw new Error("Invalid username");
}
reason = reason.substring(0, 255); reason = reason.substring(0, 255);
var chan = this.channel; var chan = this.channel;
@ -327,9 +323,6 @@ KickBanModule.prototype.banName = async function banName(actor, name, reason) {
}; };
KickBanModule.prototype.banIP = async function banIP(actor, ip, name, reason) { KickBanModule.prototype.banIP = async function banIP(actor, ip, name, reason) {
if (!util.isValidUserName(name)) {
throw new Error("Invalid username");
}
reason = reason.substring(0, 255); reason = reason.substring(0, 255);
var masked = util.cloakIP(ip); var masked = util.cloakIP(ip);
@ -411,12 +404,7 @@ KickBanModule.prototype.banAll = async function banAll(
); );
if (!await dbIsNameBanned(chan.name, name)) { if (!await dbIsNameBanned(chan.name, name)) {
promises.push(this.banName(actor, name, reason).catch(error => { promises.push(this.banName(actor, name, reason));
// TODO: banning should be made idempotent, not throw an error
if (!/already banned/.test(error.message)) {
throw error;
}
}));
} }
await Promise.all(promises); await Promise.all(promises);
@ -452,9 +440,8 @@ KickBanModule.prototype.handleUnban = function (user, data) {
self.channel.logger.log("[mod] " + user.getName() + " unbanned " + data.name); self.channel.logger.log("[mod] " + user.getName() + " unbanned " + data.name);
if (self.channel.modules.chat) { if (self.channel.modules.chat) {
var banperm = self.channel.modules.permissions.permissions.ban; var banperm = self.channel.modules.permissions.permissions.ban;
// TODO: quick fix, shouldn't trust name from unban frame.
self.channel.modules.chat.sendModMessage( self.channel.modules.chat.sendModMessage(
user.getName() + " unbanned " + XSS.sanitizeText(data.name), user.getName() + " unbanned " + data.name,
banperm banperm
); );
} }

View file

@ -1,24 +0,0 @@
import Server from '../server';
export async function handleBanChannel({ name, externalReason, internalReason }) {
await Server.getServer().bannedChannelsController.banChannel({
name,
externalReason,
internalReason,
bannedBy: '[console]'
});
return { status: 'success' };
}
export async function handleUnbanChannel({ name }) {
await Server.getServer().bannedChannelsController.unbanChannel(name, '[console]');
return { status: 'success' };
}
export async function handleShowBannedChannel({ name }) {
let banInfo = await Server.getServer().bannedChannelsController.getBannedChannel(name);
return { status: 'success', ban: banInfo };
}

View file

@ -11,8 +11,7 @@ import { CaptchaConfig } from './configuration/captchaconfig';
const LOGGER = require('@calzoneman/jsli')('config'); const LOGGER = require('@calzoneman/jsli')('config');
var defaults = { var defaults = {
database: { mysql: {
client: "mysql",
server: "localhost", server: "localhost",
port: 3306, port: 3306,
database: "cytube3", database: "cytube3",
@ -67,12 +66,12 @@ var defaults = {
} }
}, },
"youtube-v3-key": "", "youtube-v3-key": "",
"channel-blacklist": [],
"channel-path": "r", "channel-path": "r",
"channel-save-interval": 5, "channel-save-interval": 5,
"max-channels-per-user": 5, "max-channels-per-user": 5,
"max-accounts-per-ip": 5, "max-accounts-per-ip": 5,
"guest-login-delay": 60, "guest-login-delay": 60,
"max-chat-message-length": 320,
aliases: { aliases: {
"purge-interval": 3600000, "purge-interval": 3600000,
"max-age": 2592000000 "max-age": 2592000000
@ -373,6 +372,13 @@ function preprocessConfig(cfg) {
} }
} }
/* Convert channel blacklist to a hashtable */
var tbl = {};
cfg["channel-blacklist"].forEach(function (c) {
tbl[c.toLowerCase()] = true;
});
cfg["channel-blacklist"] = tbl;
/* Check channel path */ /* Check channel path */
if(!/^[-\w]+$/.test(cfg["channel-path"])){ if(!/^[-\w]+$/.test(cfg["channel-path"])){
LOGGER.error("Channel paths may only use the same characters as usernames and channel names."); LOGGER.error("Channel paths may only use the same characters as usernames and channel names.");
@ -429,11 +435,6 @@ function preprocessConfig(cfg) {
cfg['channel-storage'] = { type: undefined }; cfg['channel-storage'] = { type: undefined };
} }
if (cfg["max-chat-message-length"] > 1000) {
LOGGER.warn("Max chat message length was greater than 1000. Setting to 1000.");
cfg["max-chat-message-length"] = 1000;
}
return cfg; return cfg;
} }

View file

@ -1,67 +0,0 @@
import { eventlog } from '../logger';
import { SimpleCache } from '../util/simple-cache';
const LOGGER = require('@calzoneman/jsli')('BannedChannelsController');
export class BannedChannelsController {
constructor(dbChannels, globalMessageBus) {
this.dbChannels = dbChannels;
this.globalMessageBus = globalMessageBus;
this.cache = new SimpleCache({
maxElem: 1000,
maxAge: 5 * 60_000
});
}
/*
* TODO: add an audit log to the database
*/
async banChannel({ name, externalReason, internalReason, bannedBy }) {
LOGGER.info(`Banning channel ${name} (banned by ${bannedBy})`);
eventlog.log(`[acp] ${bannedBy} banned channel ${name}`);
let banInfo = await this.dbChannels.getBannedChannel(name);
if (banInfo !== null) {
LOGGER.warn(`Channel ${name} is already banned, updating ban reason`);
}
this.cache.delete(name);
await this.dbChannels.putBannedChannel({
name,
externalReason,
internalReason,
bannedBy
});
this.globalMessageBus.emit(
'ChannelBanned',
{ channel: name, externalReason }
);
}
async unbanChannel(name, unbannedBy) {
LOGGER.info(`Unbanning channel ${name}`);
eventlog.log(`[acp] ${unbannedBy} unbanned channel ${name}`);
this.cache.delete(name);
this.globalMessageBus.emit(
'ChannelUnbanned',
{ channel: name }
);
await this.dbChannels.removeBannedChannel(name);
}
async getBannedChannel(name) {
name = name.toLowerCase();
let info = this.cache.get(name);
if (info === null) {
info = await this.dbChannels.getBannedChannel(name);
this.cache.put(name, info);
}
return info;
}
}

View file

@ -31,6 +31,10 @@ const SOURCE_CONTENT_TYPES = new Set([
'video/webm' 'video/webm'
]); ]);
const LIVE_ONLY_CONTENT_TYPES = new Set([
'application/dash+xml'
]);
const AUDIO_ONLY_CONTENT_TYPES = new Set([ const AUDIO_ONLY_CONTENT_TYPES = new Set([
'audio/aac', 'audio/aac',
'audio/mp4', 'audio/mp4',
@ -169,7 +173,7 @@ export function validate(data) {
validateURL(data.thumbnail); validateURL(data.thumbnail);
} }
validateSources(data.sources); validateSources(data.sources, data);
validateAudioTracks(data.audioTracks); validateAudioTracks(data.audioTracks);
validateTextTracks(data.textTracks); validateTextTracks(data.textTracks);
/* /*
@ -182,7 +186,7 @@ export function validate(data) {
validateFonts(data.fonts); validateFonts(data.fonts);
} }
function validateSources(sources) { function validateSources(sources, data) {
if (!Array.isArray(sources)) if (!Array.isArray(sources))
throw new ValidationError('sources must be a list'); throw new ValidationError('sources must be a list');
if (sources.length === 0) if (sources.length === 0)
@ -198,6 +202,10 @@ function validateSources(sources) {
`unacceptable source contentType "${source.contentType}"` `unacceptable source contentType "${source.contentType}"`
); );
if (LIVE_ONLY_CONTENT_TYPES.has(source.contentType) && !data.live)
throw new ValidationError(
`contentType "${source.contentType}" requires live: true`
);
// TODO (Xaekai): This should be allowed // TODO (Xaekai): This should be allowed
if (/*!AUDIO_ONLY_CONTENT_TYPES.has(source.contentType) && */!SOURCE_QUALITIES.has(source.quality)) if (/*!AUDIO_ONLY_CONTENT_TYPES.has(source.contentType) && */!SOURCE_QUALITIES.has(source.quality))
throw new ValidationError(`unacceptable source quality "${source.quality}"`); throw new ValidationError(`unacceptable source quality "${source.quality}"`);

View file

@ -30,19 +30,19 @@ class Database {
constructor(knexConfig = null) { constructor(knexConfig = null) {
if (knexConfig === null) { if (knexConfig === null) {
knexConfig = { knexConfig = {
client: Config.get('database.client'), client: 'mysql',
connection: { connection: {
host: Config.get('database.server'), host: Config.get('mysql.server'),
port: Config.get('database.port'), port: Config.get('mysql.port'),
user: Config.get('database.user'), user: Config.get('mysql.user'),
password: Config.get('database.password'), password: Config.get('mysql.password'),
database: Config.get('database.database'), database: Config.get('mysql.database'),
multipleStatements: true, // Legacy thing multipleStatements: true, // Legacy thing
charset: 'utf8mb4' charset: 'utf8mb4'
}, },
pool: { pool: {
min: Config.get('database.pool-size'), min: Config.get('mysql.pool-size'),
max: Config.get('database.pool-size') max: Config.get('mysql.pool-size')
}, },
debug: !!process.env.KNEX_DEBUG debug: !!process.env.KNEX_DEBUG
}; };
@ -73,8 +73,6 @@ module.exports.init = function (newDB) {
} else { } else {
db = new Database(); db = new Database();
} }
// FIXME Initial database connection failed: error: select 1 from dual
// relation "dual" does not exist
db.knex.raw('select 1 from dual') db.knex.raw('select 1 from dual')
.catch(error => { .catch(error => {
LOGGER.error('Initial database connection failed: %s', error.stack); LOGGER.error('Initial database connection failed: %s', error.stack);

View file

@ -2,7 +2,6 @@ var db = require("../database");
var valid = require("../utilities").isValidChannelName; var valid = require("../utilities").isValidChannelName;
var Flags = require("../flags"); var Flags = require("../flags");
var util = require("../utilities"); var util = require("../utilities");
// TODO: I think newer knex has native support for this
import { createMySQLDuplicateKeyUpdate } from '../util/on-duplicate-key-update'; import { createMySQLDuplicateKeyUpdate } from '../util/on-duplicate-key-update';
import Config from '../config'; import Config from '../config';
@ -210,9 +209,7 @@ module.exports = {
return; return;
} }
db.query("SELECT c.*, bc.external_reason as banReason " + db.query("SELECT * FROM `channels` WHERE owner=?", [owner],
"FROM channels c LEFT OUTER JOIN banned_channels bc " +
"ON bc.channel_name = c.name WHERE c.owner=?", [owner],
function (err, res) { function (err, res) {
if (err) { if (err) {
callback(err, []); callback(err, []);
@ -248,28 +245,13 @@ module.exports = {
return; return;
} }
db.query("SELECT c.*, bc.external_reason as banReason " + db.query("SELECT * FROM `channels` WHERE name=?", chan.name, function (err, res) {
"FROM channels c LEFT OUTER JOIN banned_channels bc " +
"ON bc.channel_name = c.name WHERE c.name=? " +
"UNION " +
"SELECT c.*, bc.external_reason as banReason " +
"FROM channels c RIGHT OUTER JOIN banned_channels bc " +
"ON bc.channel_name = c.name WHERE bc.channel_name=? ",
[chan.name, chan.name],
function (err, res) {
if (err) { if (err) {
callback(err, null); callback(err, null);
return; return;
} }
if (res.length > 0 && res[0].banReason !== null) { if (res.length === 0) {
let banError = new Error(`Channel is banned: ${res[0].banReason}`);
banError.code = 'EBANNED';
callback(banError, null);
return;
}
if (res.length === 0 || res[0].id === null) {
callback("Channel is not registered", null); callback("Channel is not registered", null);
return; return;
} }
@ -722,63 +704,5 @@ module.exports = {
LOGGER.error(`Failed to update owner_last_seen column for channel ID ${channelId}: ${error}`); LOGGER.error(`Failed to update owner_last_seen column for channel ID ${channelId}: ${error}`);
} }
}); });
},
getBannedChannel: async function getBannedChannel(name) {
if (!valid(name)) {
throw new Error("Invalid channel name");
}
return await db.getDB().runTransaction(async tx => {
let rows = await tx.table('banned_channels')
.where({ channel_name: name })
.select();
if (rows.length === 0) {
return null;
}
return {
channelName: rows[0].channel_name,
externalReason: rows[0].external_reason,
internalReason: rows[0].internal_reason,
bannedBy: rows[0].banned_by,
createdAt: rows[0].created_at,
updatedAt: rows[0].updated_at
};
});
},
putBannedChannel: async function putBannedChannel({ name, externalReason, internalReason, bannedBy }) {
if (!valid(name)) {
throw new Error("Invalid channel name");
}
return await db.getDB().runTransaction(async tx => {
let insert = tx.table('banned_channels')
.insert({
channel_name: name,
external_reason: externalReason,
internal_reason: internalReason,
banned_by: bannedBy
});
let update = tx.raw(createMySQLDuplicateKeyUpdate(
['external_reason', 'internal_reason', 'banned_by']
));
return tx.raw(insert.toString() + update.toString());
});
},
removeBannedChannel: async function removeBannedChannel(name) {
if (!valid(name)) {
throw new Error("Invalid channel name");
}
return await db.getDB().runTransaction(async tx => {
await tx.table('banned_channels')
.where({ channel_name: name })
.delete();
});
} }
}; };

View file

@ -156,15 +156,4 @@ export async function initTables() {
t.primary(['type', 'id']); t.primary(['type', 'id']);
t.index('updated_at'); t.index('updated_at');
}); });
await ensureTable('banned_channels', t => {
t.charset('utf8mb4');
t.string('channel_name', 30)
.notNullable()
.unique();
t.text('external_reason').notNullable();
t.text('internal_reason').notNullable();
t.string('banned_by', 20).notNullable();
t.timestamps(/* useTimestamps */ true, /* defaultToNow */ true);
});
} }

View file

@ -2,7 +2,6 @@ import Config from './config';
import * as Switches from './switches'; import * as Switches from './switches';
import { eventlog } from './logger'; import { eventlog } from './logger';
require('source-map-support').install(); require('source-map-support').install();
import * as bannedChannels from './cli/banned-channels';
const LOGGER = require('@calzoneman/jsli')('main'); const LOGGER = require('@calzoneman/jsli')('main');
@ -29,39 +28,10 @@ if (!Config.get('debug')) {
}); });
} }
async function handleCliCmd(cmd) {
try {
switch (cmd.command) {
case 'ban-channel':
return bannedChannels.handleBanChannel(cmd);
case 'unban-channel':
return bannedChannels.handleUnbanChannel(cmd);
case 'show-banned-channel':
return bannedChannels.handleShowBannedChannel(cmd);
default:
throw new Error(`Unrecognized command "${cmd.command}"`);
}
} catch (error) {
return { status: 'error', error: String(error) };
}
}
// TODO: this can probably just be part of servsock.js // TODO: this can probably just be part of servsock.js
// servsock should also be refactored to send replies instead of // servsock should also be refactored to send replies instead of
// relying solely on tailing logs // relying solely on tailing logs
function handleLine(line, client) { function handleLine(line) {
try {
let cmd = JSON.parse(line);
handleCliCmd(cmd).then(res => {
client.write(JSON.stringify(res) + '\n');
}).catch(error => {
LOGGER.error(`Unexpected error in handleCliCmd: ${error.stack}`);
client.write('{"status":"error","error":"internal error"}\n');
});
} catch (_error) {
// eslint no-empty: off
}
if (line === '/reload') { if (line === '/reload') {
LOGGER.info('Reloading config'); LOGGER.info('Reloading config');
try { try {
@ -111,9 +81,9 @@ if (Config.get('service-socket.enabled')) {
const ServiceSocket = require('./servsock'); const ServiceSocket = require('./servsock');
const sock = new ServiceSocket(); const sock = new ServiceSocket();
sock.init( sock.init(
(line, client) => { line => {
try { try {
handleLine(line, client); handleLine(line);
} catch (error) { } catch (error) {
LOGGER.error( LOGGER.error(
'Error in UNIX socket command handler: %s', 'Error in UNIX socket command handler: %s',

View file

@ -48,7 +48,6 @@ import { PartitionModule } from './partition/partitionmodule';
import { Gauge } from 'prom-client'; import { Gauge } from 'prom-client';
import { EmailController } from './controller/email'; import { EmailController } from './controller/email';
import { CaptchaController } from './controller/captcha'; import { CaptchaController } from './controller/captcha';
import { BannedChannelsController } from './controller/banned-channels';
var Server = function () { var Server = function () {
var self = this; var self = this;
@ -72,7 +71,6 @@ var Server = function () {
const globalMessageBus = this.initModule.getGlobalMessageBus(); const globalMessageBus = this.initModule.getGlobalMessageBus();
globalMessageBus.on('UserProfileChanged', this.handleUserProfileChange.bind(this)); globalMessageBus.on('UserProfileChanged', this.handleUserProfileChange.bind(this));
globalMessageBus.on('ChannelDeleted', this.handleChannelDelete.bind(this)); globalMessageBus.on('ChannelDeleted', this.handleChannelDelete.bind(this));
globalMessageBus.on('ChannelBanned', this.handleChannelBanned.bind(this));
globalMessageBus.on('ChannelRegistered', this.handleChannelRegister.bind(this)); globalMessageBus.on('ChannelRegistered', this.handleChannelRegister.bind(this));
// database init ------------------------------------------------------ // database init ------------------------------------------------------
@ -110,11 +108,6 @@ var Server = function () {
Config.getCaptchaConfig() Config.getCaptchaConfig()
); );
self.bannedChannelsController = new BannedChannelsController(
self.db.channels,
globalMessageBus
);
// webserver init ----------------------------------------------------- // webserver init -----------------------------------------------------
const ioConfig = IOConfiguration.fromOldConfig(Config); const ioConfig = IOConfiguration.fromOldConfig(Config);
const webConfig = WebConfiguration.fromOldConfig(Config); const webConfig = WebConfiguration.fromOldConfig(Config);
@ -141,8 +134,7 @@ var Server = function () {
Config.getEmailConfig(), Config.getEmailConfig(),
emailController, emailController,
Config.getCaptchaConfig(), Config.getCaptchaConfig(),
captchaController, captchaController
self.bannedChannelsController
); );
// http/https/sio server init ----------------------------------------- // http/https/sio server init -----------------------------------------
@ -557,34 +549,6 @@ Server.prototype.handleChannelDelete = function (event) {
} }
}; };
Server.prototype.handleChannelBanned = function (event) {
try {
const lname = event.channel.toLowerCase();
const reason = event.externalReason;
this.channels.forEach(channel => {
if (channel.dead) return;
if (channel.uniqueName === lname) {
channel.clearFlag(Flags.C_REGISTERED);
const users = Array.prototype.slice.call(channel.users);
users.forEach(u => {
u.kick(`Channel was banned: ${reason}`);
});
if (!channel.dead && !channel.dying) {
channel.emit('empty');
}
LOGGER.info('Processed banned channel %s', lname);
}
});
} catch (error) {
LOGGER.error('handleChannelBanned failed: %s', error);
}
};
Server.prototype.handleChannelRegister = function (event) { Server.prototype.handleChannelRegister = function (event) {
try { try {
const lname = event.channel.toLowerCase(); const lname = event.channel.toLowerCase();

View file

@ -34,7 +34,7 @@ export default class ServiceSocket {
delete this.connections[id]; delete this.connections[id];
}); });
stream.on('data', (msg) => { stream.on('data', (msg) => {
this.handler(msg.toString(), stream); this.handler(msg.toString());
}); });
}).listen(this.socket); }).listen(this.socket);
process.on('exit', this.closeServiceSocket.bind(this)); process.on('exit', this.closeServiceSocket.bind(this));

View file

@ -76,6 +76,10 @@ User.prototype.handleJoinChannel = function handleJoinChannel(data) {
} }
data.name = data.name.toLowerCase(); data.name = data.name.toLowerCase();
if (data.name in Config.get("channel-blacklist")) {
this.kick("This channel is blacklisted.");
return;
}
this.waitFlag(Flags.U_READY, () => { this.waitFlag(Flags.U_READY, () => {
var chan; var chan;
@ -98,6 +102,10 @@ User.prototype.handleJoinChannel = function handleJoinChannel(data) {
if (!chan.is(Flags.C_READY)) { if (!chan.is(Flags.C_READY)) {
chan.once("loadFail", reason => { chan.once("loadFail", reason => {
this.socket.emit("errorMsg", {
msg: reason,
alert: true
});
this.kick(`Channel could not be loaded: ${reason}`); this.kick(`Channel could not be loaded: ${reason}`);
}); });
} }

View file

@ -1,45 +0,0 @@
class SimpleCache {
constructor({ maxElem, maxAge }) {
this.maxElem = maxElem;
this.maxAge = maxAge;
this.cache = new Map();
setInterval(() => {
this.cleanup();
}, maxAge).unref();
}
put(key, value) {
this.cache.set(key, { value: value, at: Date.now() });
if (this.cache.size > this.maxElem) {
this.cache.delete(this.cache.keys().next().value);
}
}
get(key) {
let val = this.cache.get(key);
if (val != null && Date.now() < val.at + this.maxAge) {
return val.value;
} else {
return null;
}
}
delete(key) {
this.cache.delete(key);
}
cleanup() {
let now = Date.now();
for (let [key, value] of this.cache) {
if (value.at < now - this.maxAge) {
this.cache.delete(key);
}
}
}
}
export { SimpleCache };

View file

@ -265,8 +265,6 @@ async function handleNewChannel(req, res) {
}); });
} }
let banInfo = await db.channels.getBannedChannel(name);
db.channels.listUserChannels(user.name, function (err, channels) { db.channels.listUserChannels(user.name, function (err, channels) {
if (err) { if (err) {
sendPug(res, "account-channels", { sendPug(res, "account-channels", {
@ -276,14 +274,6 @@ async function handleNewChannel(req, res) {
return; return;
} }
if (banInfo !== null) {
sendPug(res, "account-channels", {
channels: channels,
newChannelError: `Cannot register "${name}": this channel is banned.`
});
return;
}
if (name.match(Config.get("reserved-names.channels"))) { if (name.match(Config.get("reserved-names.channels"))) {
sendPug(res, "account-channels", { sendPug(res, "account-channels", {
channels: channels, channels: channels,

View file

@ -1,25 +1,16 @@
import CyTubeUtil from '../../utilities'; import CyTubeUtil from '../../utilities';
import Config from '../../config';
import { sanitizeText } from '../../xss'; import { sanitizeText } from '../../xss';
import { sendPug } from '../pug'; import { sendPug } from '../pug';
import * as HTTPStatus from '../httpstatus'; import * as HTTPStatus from '../httpstatus';
import { HTTPError } from '../../errors'; import { HTTPError } from '../../errors';
export default function initialize(app, ioConfig, chanPath, getBannedChannel) { export default function initialize(app, ioConfig, chanPath) {
app.get(`/${chanPath}/:channel`, async (req, res) => { app.get(`/${chanPath}/:channel`, (req, res) => {
if (!req.params.channel || !CyTubeUtil.isValidChannelName(req.params.channel)) { if (!req.params.channel || !CyTubeUtil.isValidChannelName(req.params.channel)) {
throw new HTTPError(`"${sanitizeText(req.params.channel)}" is not a valid ` + throw new HTTPError(`"${sanitizeText(req.params.channel)}" is not a valid ` +
'channel name.', { status: HTTPStatus.NOT_FOUND }); 'channel name.', { status: HTTPStatus.NOT_FOUND });
} }
let banInfo = await getBannedChannel(req.params.channel);
if (banInfo !== null) {
sendPug(res, 'banned_channel', {
externalReason: banInfo.externalReason
});
return;
}
const endpoints = ioConfig.getSocketEndpoints(); const endpoints = ioConfig.getSocketEndpoints();
if (endpoints.length === 0) { if (endpoints.length === 0) {
throw new HTTPError('No socket.io endpoints configured'); throw new HTTPError('No socket.io endpoints configured');
@ -28,8 +19,7 @@ export default function initialize(app, ioConfig, chanPath, getBannedChannel) {
sendPug(res, 'channel', { sendPug(res, 'channel', {
channelName: req.params.channel, channelName: req.params.channel,
sioSource: `${socketBaseURL}/socket.io/socket.io.js`, sioSource: `${socketBaseURL}/socket.io/socket.io.js`
maxMsgLen: Config.get("max-chat-message-length")
}); });
}); });
} }

View file

@ -143,8 +143,7 @@ module.exports = {
emailConfig, emailConfig,
emailController, emailController,
captchaConfig, captchaConfig,
captchaController, captchaController
bannedChannelsController
) { ) {
patchExpressToHandleAsync(); patchExpressToHandleAsync();
const chanPath = Config.get('channel-path'); const chanPath = Config.get('channel-path');
@ -195,12 +194,7 @@ module.exports = {
LOGGER.info('Enabled express-minify for CSS and JS'); LOGGER.info('Enabled express-minify for CSS and JS');
} }
require('./routes/channel')( require('./routes/channel')(app, ioConfig, chanPath);
app,
ioConfig,
chanPath,
async name => bannedChannelsController.getBannedChannel(name)
);
require('./routes/index')(app, channelIndex, webConfig.getMaxIndexEntries()); require('./routes/index')(app, channelIndex, webConfig.getMaxIndexEntries());
require('./routes/socketconfig')(app, clusterClient); require('./routes/socketconfig')(app, clusterClient);
require('./routes/contact')(app, webConfig); require('./routes/contact')(app, webConfig);

View file

@ -24,7 +24,7 @@ block content
tbody tbody
for c in channels for c in channels
tr tr
td th
form.form-inline.pull-right(action="/account/channels", method="post", onsubmit="return confirm('Are you sure you want to delete " +c.name+ "? This cannot be undone');") form.form-inline.pull-right(action="/account/channels", method="post", onsubmit="return confirm('Are you sure you want to delete " +c.name+ "? This cannot be undone');")
input(type="hidden", name="_csrf", value=csrfToken) input(type="hidden", name="_csrf", value=csrfToken)
input(type="hidden", name="action", value="delete_channel") input(type="hidden", name="action", value="delete_channel")
@ -32,9 +32,6 @@ block content
button.btn.btn-xs.btn-danger(type="submit") Delete button.btn.btn-xs.btn-danger(type="submit") Delete
span.glyphicon.glyphicon-trash span.glyphicon.glyphicon-trash
a(href=`/${channelPath}/${c.name}`, style="margin-left: 5px")= c.name a(href=`/${channelPath}/${c.name}`, style="margin-left: 5px")= c.name
if c.banReason != null
| &nbsp;
span.label.label-danger Banned
.col-lg-6.col-md-6 .col-lg-6.col-md-6
h3 Register a new channel h3 Register a new channel
if newChannelError if newChannelError

View file

@ -1,9 +0,0 @@
extends layout.pug
block content
.col-md-12
.alert.alert-danger
h1 Banned Channel
strong This channel is banned:
p
= externalReason

View file

@ -46,7 +46,7 @@ html(lang="en")
#userlist #userlist
#messagebuffer.linewrap #messagebuffer.linewrap
form(action="javascript:void(0)") form(action="javascript:void(0)")
input#chatline.form-control(type="text", maxlength=maxMsgLen, style="display: none") input#chatline.form-control(type="text", maxlength="320", style="display: none")
#guestlogin.input-group #guestlogin.input-group
span.input-group-addon Guest login span.input-group-addon Guest login
input#guestname.form-control(type="text", placeholder="Name") input#guestname.form-control(type="text", placeholder="Name")

View file

@ -1,52 +0,0 @@
const { SimpleCache } = require('../../lib/util/simple-cache');
const assert = require('assert');
describe('SimpleCache', () => {
const CACHE_MAX_ELEM = 5;
const CACHE_MAX_AGE = 5;
let cache;
beforeEach(() => {
cache = new SimpleCache({
maxElem: CACHE_MAX_ELEM,
maxAge: CACHE_MAX_AGE
});
});
it('sets, gets, and deletes a value', () => {
assert.strictEqual(cache.get('foo'), null);
cache.put('foo', 'bar');
assert.strictEqual(cache.get('foo'), 'bar');
cache.delete('foo');
assert.strictEqual(cache.get('foo'), null);
});
it('does not return an expired value', done => {
cache.put('foo', 'bar');
setTimeout(() => {
assert.strictEqual(cache.get('foo'), null);
done();
}, CACHE_MAX_AGE + 1);
});
it('cleans up old values', done => {
cache.put('foo', 'bar');
setTimeout(() => {
assert.strictEqual(cache.get('foo'), null);
done();
}, CACHE_MAX_AGE * 2);
});
it('removes the oldest entry if max elem is reached', () => {
for (let i = 0; i < CACHE_MAX_ELEM + 1; i++) {
cache.put(`foo${i}`, 'bar');
}
assert.strictEqual(cache.get('foo0'), null);
assert.strictEqual(cache.get('foo1'), 'bar');
});
});

View file

@ -1,55 +1,4 @@
/*eslint no-unused-vars: "off"*/ /*eslint no-unused-vars: "off"*/
(function() {
/**
* Test whether the browser supports nullish-coalescing operator.
*
* Users with old browsers will probably fail to load the client correctly
* because parsing this operator in older browsers results in a SyntaxError
* that aborts compilation of the entire script (not just an exception where
* it is used). In particular, as of 2023-01-28, Utherverse ships with
* a rather old browser version (Chrome 76) and several users have reported
* it not working.
*/
try {
try {
new Function('x?.y');
} catch (e) {
if (e.name === 'SyntaxError') {
/**
* If we're at this point, we can't be sure what scripts have
* actually loaded, so construct the error alert the old
* fashioned way.
*/
var wrap = document.createElement('div');
wrap.className = 'col-md-12';
var al = document.createElement('div');
al.className = 'alert alert-danger';
var title = document.createElement('strong');
title.textContent = 'Unsupported Browser';
var msg = document.createElement('p');
msg.textContent = 'It looks like your browser does not support ' +
'the required JavaScript features to run ' +
'CyTube. This is usually caused by ' +
'using an outdated browser version. Please '+
'check if an update is available. Your ' +
'browser version is reported as:';
var version = document.createElement('tt');
version.textContent = navigator.userAgent;
wrap.appendChild(al);
al.appendChild(title);
al.appendChild(msg);
al.appendChild(document.createElement('br'));
al.appendChild(version);
document.getElementById('motdrow').appendChild(wrap);
}
}
} catch (e) {
console.error('Error probing for feature support:', e.stack);
}
})();
var CL_VERSION = 3.0; var CL_VERSION = 3.0;
var GS_VERSION = 1.7; // Google Drive Userscript var GS_VERSION = 1.7; // Google Drive Userscript

View file

@ -1287,11 +1287,6 @@ function playlistMove(from, after, cb) {
} }
} }
function checkYP(id) {
if (!/^(PL[a-zA-Z0-9_-]{32}|PL[A-F0-9]{16}|OLA[a-zA-Z0-9_-]{38})$/.test(id)) {
throw new Error('Invalid YouTube Playlist ID. Note that only regular user-created playlists are supported.');
}
}
function parseMediaLink(url) { function parseMediaLink(url) {
function parseShortCode(url){ function parseShortCode(url){
@ -1306,9 +1301,6 @@ function parseMediaLink(url) {
case 'fi': case 'fi':
case 'cm': case 'cm':
return { type, id }; return { type, id };
case 'yp':
checkYP(id);
return { type, id };
// Generic for the rest. // Generic for the rest.
default: default:
return { type, id: id.match(/([^\?&#]+)/)[1] }; return { type, id: id.match(/([^\?&#]+)/)[1] };
@ -1364,7 +1356,6 @@ function parseMediaLink(url) {
return { type: 'yt', id: data.pathname.slice(8,19) } return { type: 'yt', id: data.pathname.slice(8,19) }
} }
if(data.pathname == '/playlist'){ if(data.pathname == '/playlist'){
checkYP(data.searchParams.get('list'));
return { type: 'yp', id: data.searchParams.get('list') } return { type: 'yp', id: data.searchParams.get('list') }
} }
case 'youtu.be': case 'youtu.be':