diff --git a/package.json b/package.json index e15ad60c..7241711f 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "Calvin Montgomery", "name": "CyTube", "description": "Online media synchronizer and chat", - "version": "3.31.0", + "version": "3.32.0", "repository": { "url": "http://github.com/calzoneman/sync" }, diff --git a/src/database.js b/src/database.js index f155ee79..243c7f58 100644 --- a/src/database.js +++ b/src/database.js @@ -48,6 +48,7 @@ module.exports.init = function () { global_ipbans = {}; module.exports.users = require("./database/accounts"); module.exports.channels = require("./database/channels"); + module.exports.pool = pool; }; /** diff --git a/src/database/accounts.js b/src/database/accounts.js index ce7f015f..3d6b2c28 100644 --- a/src/database/accounts.js +++ b/src/database/accounts.js @@ -7,15 +7,6 @@ var Logger = require("../logger"); var registrationLock = {}; var blackHole = function () { }; -/** - * Replaces look-alike characters with "_" (single character wildcard) for - * use in LIKE queries. This prevents guests from taking names that look - * visually identical to existing names in certain fonts. - */ -function wildcardSimilarChars(name) { - return name.replace(/_/g, "\\_").replace(/[Il1oO0]/g, "_"); -} - function parseProfile(data) { try { var profile = JSON.parse(data.profile); @@ -31,12 +22,23 @@ module.exports = { init: function () { }, + /** + * Convert a username for deduplication purposes. + * Collapses visibily similar characters into a single character. + * @param name + */ + dedupeUsername: function dedupeUsername(name) { + return name.replace(/[Il1]/ig, '1') + .replace(/[o0]/ig, '0') + .replace(/[_-]/g, '_'); + }, + /** * Check if a username is taken */ isUsernameTaken: function (name, callback) { - db.query("SELECT name FROM `users` WHERE name LIKE ? ESCAPE '\\\\'", - [wildcardSimilarChars(name)], + db.query("SELECT name FROM `users` WHERE name = ? or name_dedupe = ?", + [name, module.exports.dedupeUsername(name)], function (err, rows) { if (err) { callback(err, true); @@ -175,10 +177,10 @@ module.exports = { } db.query("INSERT INTO `users` " + - "(`name`, `password`, `global_rank`, `email`, `profile`, `ip`, `time`)" + + "(`name`, `password`, `global_rank`, `email`, `profile`, `ip`, `time`, `name_dedupe`)" + " VALUES " + - "(?, ?, ?, ?, '', ?, ?)", - [name, hash, 1, email, ip, Date.now()], + "(?, ?, ?, ?, '', ?, ?, ?)", + [name, hash, 1, email, ip, Date.now(), module.exports.dedupeUsername(name)], function (err, res) { delete registrationLock[lname]; if (err) { diff --git a/src/database/tables.js b/src/database/tables.js index 140e50c3..e0874a38 100644 --- a/src/database/tables.js +++ b/src/database/tables.js @@ -8,8 +8,10 @@ const TBL_USERS = "" + "`profile` TEXT CHARACTER SET utf8mb4 NOT NULL," + "`ip` VARCHAR(39) NOT NULL," + "`time` BIGINT NOT NULL," + + "`name_dedupe` VARCHAR(20) DEFAULT NULL," + "PRIMARY KEY(`id`)," + - "UNIQUE(`name`)) " + + "UNIQUE(`name`)," + + "UNIQUE(`name_dedupe`)) " + "CHARACTER SET utf8"; const TBL_CHANNELS = "" + diff --git a/src/database/update.js b/src/database/update.js index 340f2c60..b09efff1 100644 --- a/src/database/update.js +++ b/src/database/update.js @@ -1,8 +1,9 @@ var db = require("../database"); var Logger = require("../logger"); var Q = require("q"); +import Promise from 'bluebird'; -const DB_VERSION = 7; +const DB_VERSION = 9; var hasUpdates = []; module.exports.checkVersion = function () { @@ -58,6 +59,10 @@ function update(version, cb) { fixCustomEmbeds(cb); } else if (version < 7) { fixCustomEmbedsInUserPlaylists(cb); + } else if (version < 8) { + addUsernameDedupeColumn(cb); + } else if (version < 9) { + populateUsernameDedupeColumn(cb); } } @@ -330,3 +335,56 @@ function fixCustomEmbedsInUserPlaylists(cb) { Logger.errlog.log(err.stack); }); } + +function addUsernameDedupeColumn(cb) { + Logger.syslog.log("Adding name_dedupe column on the users table"); + db.query("ALTER TABLE users ADD COLUMN name_dedupe VARCHAR(20) UNIQUE DEFAULT NULL", (error) => { + if (error) { + Logger.errlog.log(`Unable to add name_dedupe column: ${error}`); + } else { + cb(); + } + }); +} + +function populateUsernameDedupeColumn(cb) { + const dbUsers = require("./accounts"); + Logger.syslog.log("Populating name_dedupe column on the users table"); + db.query("SELECT id, name FROM users WHERE name_dedupe IS NULL", (err, rows) => { + if (err) { + Logger.errlog.log("Unable to perform database upgrade to add dedupe column: " + err); + return; + } + + Promise.map(rows, row => { + return new Promise((resolve, reject) => { + db.pool.getConnection((error, conn) => { + if (error) { + reject(error); + return; + } + + const dedupedName = dbUsers.dedupeUsername(row.name); + Logger.syslog.log(`Deduping [${row.name}] as [${dedupedName}]`); + conn.query("UPDATE users SET name_dedupe = ? WHERE id = ?", [dedupedName, row.id], (error, res) => { + conn.release(); + if (error) { + if (error.errno === 1062) { + Logger.syslog.log(`WARNING: could not set name_dedupe for [${row.name}] due to an existing row for [${dedupedName}]`); + resolve(); + } else { + reject(error); + } + } else { + resolve(); + } + }); + }); + }); + }, { concurrency: 10 }).then(() => { + cb(); + }).catch(error => { + Logger.errlog.log("Unable to perform database upgrade to add dedupe column: " + (error.stack ? error.stack : error)); + }) + }); +}