"use strict"; var db = require('./database'), async = require('async'), winston = require('winston'), User = require('./user'), Topics = require('./topics'), Posts = require('./posts'), Categories = require('./categories'), Groups = require('./groups'), Meta = require('./meta'), Plugins = require('./plugins'), Utils = require('../public/src/utils'), Upgrade = {}, minSchemaDate = new Date(2014, 0, 4).getTime(), // This value gets updated every new MINOR version schemaDate, thisSchemaDate, // IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema latestSchema = new Date(2014, 1, 22).getTime(); Upgrade.check = function(callback) { db.get('schemaDate', function(err, value) { if(!value) { db.set('schemaDate', latestSchema, function(err) { callback(true); }); return; } if (parseInt(value, 10) >= latestSchema) { callback(true); } else { callback(false); } }); }; Upgrade.upgrade = function(callback) { var updatesMade = false; winston.info('Beginning database schema update'); async.series([ function(next) { // Prepare for upgrade & check to make sure the upgrade is possible db.get('schemaDate', function(err, value) { if(!value) { db.set('schemaDate', latestSchema, function(err) { next(); }); schemaDate = latestSchema; } else { schemaDate = parseInt(value, 10); } if (schemaDate >= minSchemaDate) { next(); } else { next(new Error('upgrade-not-possible')); } }); }, function(next) { thisSchemaDate = new Date(2014, 0, 5).getTime(); if (schemaDate < thisSchemaDate) { updatesMade = true; db.getListRange('categories:cid', 0, -1, function(err, cids) { if(err) { return next(err); } var timestamp = Date.now(); function upgradeCategory(cid, next) { db.getSetMembers('cid:' + cid + ':active_users', function(err, uids) { if(err) { return next(err); } db.delete('cid:' + cid + ':active_users', function(err) { if(err) { return next(err); } for(var i=0; i<uids.length; ++i) { db.sortedSetAdd('cid:' + cid + ':active_users', timestamp, uids[i]); } next(); }); }); } async.each(cids, upgradeCategory, function(err) { if(err) { return next(err); } winston.info('[2014/1/5] Upgraded categories active users'); next(); }); }); } else { winston.info('[2014/1/5] categories active users skipped'); next(); } }, function(next) { thisSchemaDate = new Date(2014, 0, 5, 14, 6).getTime(); if (schemaDate < thisSchemaDate) { updatesMade = true; // Re-slugify all users db.delete('userslug:uid', function(err) { if (!err) { db.getObjectValues('username:uid', function(err, uids) { var newUserSlug; async.each(uids, function(uid, next) { User.getUserField(uid, 'username', function(err, username) { if(err) { return next(err); } if(username) { newUserSlug = Utils.slugify(username); async.parallel([ function(next) { User.setUserField(uid, 'userslug', newUserSlug, next); }, function(next) { db.setObjectField('userslug:uid', newUserSlug, uid, next); } ], next); } else { winston.warn('uid '+ uid + ' doesn\'t have a valid username (' + username + '), skipping'); next(null); } }); }, function(err) { winston.info('[2014/1/5] Re-slugify usernames (again)'); next(err); }); }); } }); } else { winston.info('[2014/1/5] Re-slugify usernames (again) skipped'); next(); } }, function(next) { function upgradeUserPostsTopics(next) { function upgradeUser(uid, next) { function upgradeUserPosts(next) { function addPostToUser(pid) { Posts.getPostField(pid, 'timestamp', function(err, timestamp) { db.sortedSetAdd('uid:' + uid + ':posts', timestamp, pid); }); } db.getListRange('uid:' + uid + ':posts', 0, -1, function(err, pids) { if(err) { return next(err); } if(!pids || !pids.length) { return next(); } db.delete('uid:' + uid + ':posts', function(err) { for(var i = 0; i< pids.length; ++i) { addPostToUser(pids[i]); } next(); }); }); } function upgradeUserTopics(next) { function addTopicToUser(tid) { Topics.getTopicField(tid, 'timestamp', function(err, timestamp) { db.sortedSetAdd('uid:' + uid + ':topics', timestamp, tid); }); } db.getListRange('uid:' + uid + ':topics', 0, -1, function(err, tids) { if(err) { return next(err); } if(!tids || !tids.length) { return next(); } db.delete('uid:' + uid + ':topics', function(err) { for(var i = 0; i< tids.length; ++i) { addTopicToUser(tids[i]); } next(); }); }); } async.series([upgradeUserPosts, upgradeUserTopics], function(err, result) { next(err); }); } db.getSortedSetRange('users:joindate', 0, -1, function(err, uids) { if(err) { return next(err); } async.each(uids, upgradeUser, function(err, result) { next(err); }); }); } function upgradeTopicPosts(next) { function upgradeTopic(tid, next) { function addPostToTopic(pid) { Posts.getPostField(pid, 'timestamp', function(err, timestamp) { db.sortedSetAdd('tid:' + tid + ':posts', timestamp, pid); }); } db.getListRange('tid:' + tid + ':posts', 0, -1, function(err, pids) { if(err) { return next(err); } if(!pids || !pids.length) { return next(); } db.delete('tid:' + tid + ':posts', function(err) { for(var i = 0; i< pids.length; ++i) { addPostToTopic(pids[i]); } next(); }); }); } db.getSetMembers('topics:tid', function(err, tids) { async.each(tids, upgradeTopic, function(err, results) { next(err); }); }); } thisSchemaDate = new Date(2014, 0, 7).getTime(); if (schemaDate < thisSchemaDate) { updatesMade = true; async.series([upgradeUserPostsTopics, upgradeTopicPosts], function(err, results) { if(err) { winston.err('Error upgrading '+ err.message); return next(err); } winston.info('[2014/1/7] Updated topic and user posts to sorted set'); next(); }); } else { winston.info('[2014/1/7] Update to topic and user posts to sorted set skipped'); next(); } }, function(next) { thisSchemaDate = new Date(2014, 0, 13, 12, 0).getTime(); if (schemaDate < thisSchemaDate) { updatesMade = true; db.getObjectValues('username:uid', function(err, uids) { async.eachSeries(uids, function(uid, next) { Groups.joinByGroupName('registered-users', uid, next); }, function(err) { if(err) { winston.err('Error upgrading '+ err.message); process.exit(); } else { winston.info('[2014/1/13] Set up "Registered Users" user group'); next(); } }); }); } else { winston.info('[2014/1/13] Set up "Registered Users" user group - skipped'); next(); } }, function(next) { thisSchemaDate = new Date(2014, 0, 19, 22, 19).getTime(); if (schemaDate < thisSchemaDate) { updatesMade = true; db.getObjectValues('username:uid', function(err, uids) { async.each(uids, function(uid, next) { db.searchRemove('user', uid, next); }, function(err) { winston.info('[2014/1/19] Remove user search from Reds'); next(); }); }); } else { winston.info('[2014/1/19] Remove user search from Reds -- skipped'); next(); } }, function(next) { thisSchemaDate = new Date(2014, 0, 23, 16, 5).getTime(); if (schemaDate < thisSchemaDate) { updatesMade = true; Groups.getByGroupName('Administrators', {}, function(err, groupObj) { if (err && err.message === 'gid-not-found') { winston.info('[2014/1/23] Updating Administrators Group -- skipped'); return next(); } Groups.update(groupObj.gid, { name: 'administrators', hidden: '1' }, function() { winston.info('[2014/1/23] Updating Administrators Group'); next(); }); }); } else { winston.info('[2014/1/23] Updating Administrators Group -- skipped'); next(); } }, function(next) { thisSchemaDate = new Date(2014, 0, 25, 0, 0).getTime(); if (schemaDate < thisSchemaDate) { updatesMade = true; db.getSortedSetRange('users:joindate', 0, -1, function(err, uids) { if(err) { return next(err); } if(!uids || !uids.length) { winston.info('[2014/1/25] Updating User Gravatars to HTTPS -- skipped'); return next(); } var gravatar = require('gravatar'); function updateGravatar(uid, next) { User.getUserFields(uid, ['email', 'picture', 'gravatarpicture'], function(err, userData) { var gravatarPicture = User.createGravatarURLFromEmail(userData.email); if(userData.picture === userData.gravatarpicture) { User.setUserField(uid, 'picture', gravatarPicture); } User.setUserField(uid, 'gravatarpicture', gravatarPicture, next); }); } winston.info('[2014/1/25] Updating User Gravatars to HTTPS'); async.each(uids, updateGravatar, next); }); } else { winston.info('[2014/1/25] Updating User Gravatars to HTTPS -- skipped'); next(); } }, function(next) { thisSchemaDate = new Date(2014, 0, 27, 12, 35).getTime(); if (schemaDate < thisSchemaDate) { updatesMade = true; var activations = []; if (Meta.config['social:facebook:secret'] && Meta.config['social:facebook:app_id']) { activations.push(function(next) { Plugins.toggleActive('nodebb-plugin-sso-facebook', function(result) { winston.info('[2014/1/25] Activating Facebook SSO Plugin'); next(); }); }); } if (Meta.config['social:twitter:key'] && Meta.config['social:twitter:secret']) { activations.push(function(next) { Plugins.toggleActive('nodebb-plugin-sso-twitter', function(result) { winston.info('[2014/1/25] Activating Twitter SSO Plugin'); next(); }); }); } if (Meta.config['social:google:secret'] && Meta.config['social:google:id']) { activations.push(function(next) { Plugins.toggleActive('nodebb-plugin-sso-google', function(result) { winston.info('[2014/1/25] Activating Google SSO Plugin'); next(); }); }); } async.parallel(activations, function(err) { if (!err) { winston.info('[2014/1/25] Done activating SSO plugins'); } next(err); }); } else { winston.info('[2014/1/25] Activating SSO plugins, if set up -- skipped'); next(); } }, function(next) { thisSchemaDate = new Date(2014, 0, 30, 15, 0).getTime(); if (schemaDate < thisSchemaDate) { updatesMade = true; if (Meta.config.defaultLang === 'en') { Meta.configs.set('defaultLang', 'en_GB', next); } else if (Meta.config.defaultLang === 'pt_br') { Meta.configs.set('defaultLang', 'pt_BR', next); } else if (Meta.config.defaultLang === 'zh_cn') { Meta.configs.set('defaultLang', 'zh_CN', next); } else if (Meta.config.defaultLang === 'zh_tw') { Meta.configs.set('defaultLang', 'zh_TW', next); } else { winston.info('[2014/1/30] Fixing language settings -- skipped'); return next(); } winston.info('[2014/1/30] Fixing language settings'); next(); } else { winston.info('[2014/1/30] Fixing language settings -- skipped'); next(); } }, function(next) { thisSchemaDate = new Date(2014, 0, 30, 16, 0).getTime(); function updateTopic(tid, next) { Topics.getTopicFields(tid, ['postcount', 'viewcount'], function(err, topicData) { if(err) { next(err); } if(topicData) { if(!topicData.postcount) { topicData.postcount = 0; } if(!topicData.viewcount) { topicData.viewcount = 0; } db.sortedSetAdd('topics:posts', topicData.postcount, tid); db.sortedSetAdd('topics:views', topicData.viewcount, tid); } next(); }); } if (schemaDate < thisSchemaDate) { updatesMade = true; winston.info('[2014/1/30] Adding new topic sets'); db.getSortedSetRange('topics:recent', 0, -1, function(err, tids) { if(err) { return next(err); } async.each(tids, updateTopic, function(err) { next(err); }); }); } else { winston.info('[2014/1/30] Adding new topic sets -- skipped'); next(); } }, function(next) { thisSchemaDate = new Date(2014, 1, 2, 16, 0).getTime(); if (schemaDate < thisSchemaDate) { updatesMade = true; winston.info('[2014/2/6] Upvoting all favourited posts for each user'); User.getUsers('users:joindate', 0, -1, function (err, users) { function getFavourites(user, next) { function upvote(post, next) { var pid = post.pid, uid = user.uid; if (post.uid !== uid) { db.setAdd('pid:' + pid + ':upvote', uid); db.sortedSetAdd('uid:' + uid + ':upvote', post.timestamp, pid); db.incrObjectField('post:' + pid, 'votes'); } next(); } Posts.getFavourites(user.uid, 0, -1, function(err, posts) { async.each(posts.posts, upvote, function(err) { next(err); }); }); } async.each(users, getFavourites, function(err) { next(err); }); }); } else { winston.info('[2014/2/6] Upvoting all favourited posts for each user -- skipped'); next(); } }, function(next) { thisSchemaDate = new Date(2014, 1, 7, 16, 0).getTime(); if (schemaDate < thisSchemaDate) { updatesMade = true; winston.info('[2014/2/7] Updating category recent replies'); db.getListRange('categories:cid', 0, -1, function(err, cids) { function updateCategory(cid, next) { db.getSortedSetRevRange('categories:recent_posts:cid:' + cid, 0, - 1, function(err, pids) { function updatePid(pid, next) { Posts.getCidByPid(pid, function(err, realCid) { if(err) { return next(err); } if(parseInt(realCid, 10) !== parseInt(cid, 10)) { Posts.getPostField(pid, 'timestamp', function(err, timestamp) { db.sortedSetRemove('categories:recent_posts:cid:' + cid, pid); db.sortedSetAdd('categories:recent_posts:cid:' + realCid, timestamp, pid); next(); }); } else { next(); } }); } async.each(pids, updatePid, next); }); } if(err) { return next(err); } async.each(cids, updateCategory, next); }); } else { winston.info('[2014/2/7] Updating category recent replies -- skipped'); next(); } }, function(next) { thisSchemaDate = new Date(2014, 1, 9, 20, 50).getTime(); if (schemaDate < thisSchemaDate) { updatesMade = true; db.delete('tid:lastFeedUpdate', function(err) { if(err) { winston.err('Error upgrading '+ err.message); process.exit(); } else { winston.info('[2014/2/9] Remove Topic LastFeedUpdate value, as feeds are now on-demand'); next(); } }); } else { winston.info('[2014/2/9] Remove Topic LastFeedUpdate value, as feeds are now on-demand - skipped'); next(); } }, function(next) { thisSchemaDate = new Date(2014, 1, 14, 20, 50).getTime(); if (schemaDate < thisSchemaDate) { updatesMade = true; db.exists('topics:tid', function(err, exists) { if(err) { return next(err); } if(!exists) { winston.info('[2014/2/14] Upgraded topics to sorted set - skipped'); return next(); } db.getSetMembers('topics:tid', function(err, tids) { if(err) { return next(err); } if(!Array.isArray(tids)) { winston.info('[2014/2/14] Upgraded topics to sorted set - skipped (cant find any tids)'); return next(); } db.rename('topics:tid', 'topics:tid:old', function(err) { if(err) { return next(err); } async.each(tids, function(tid, next) { Topics.getTopicField(tid, 'timestamp', function(err, timestamp) { db.sortedSetAdd('topics:tid', timestamp, tid, next); }); }, function(err) { if(err) { return next(err); } winston.info('[2014/2/14] Upgraded topics to sorted set'); db.delete('topics:tid:old', next); }); }); }); }); } else { winston.info('[2014/2/14] Upgrade topics to sorted set - skipped'); next(); } }, function(next) { thisSchemaDate = new Date(2014, 1, 14, 21, 50).getTime(); if (schemaDate < thisSchemaDate) { updatesMade = true; db.exists('users:joindate', function(err, exists) { if(err) { return next(err); } if(!exists) { winston.info('[2014/2/14] Added posts to sorted set - skipped'); return next(); } db.getSortedSetRange('users:joindate', 0, -1, function(err, uids) { if(err) { return next(err); } if(!Array.isArray(uids)) { winston.info('[2014/2/14] Add posts to sorted set - skipped (cant find any uids)'); return next(); } async.each(uids, function(uid, next) { User.getPostIds(uid, 0, -1, function(err, pids) { if(err) { return next(err); } async.each(pids, function(pid, next) { Posts.getPostField(pid, 'timestamp', function(err, timestamp) { if(err) { return next(err); } db.sortedSetAdd('posts:pid', timestamp, pid, next); }); }, next); }); }, function(err) { if(err) { return next(err); } winston.info('[2014/2/14] Added posts to sorted set'); next(); }); }); }); } else { winston.info('[2014/2/14] Added posts to sorted set - skipped'); next(); } }, function(next) { thisSchemaDate = new Date(2014, 1, 19, 18, 15).getTime(); if (schemaDate < thisSchemaDate) { updatesMade = true; db.setObjectField('widgets:home.tpl', 'motd', JSON.stringify([ { "widget": "html", "data": { "html": Meta.config['motd'] || "Welcome to NodeBB, if you are an administrator of this forum visit the <a target='_blank' href='/admin/themes'>Themes</a> ACP to modify and add widgets." } } ]), function(err) { Meta.configs.remove('motd'); Meta.configs.remove('motd_class'); Meta.configs.remove('show_motd'); winston.info('[2014/2/19] Updated MOTD to use the HTML widget.'); next(err); }); } else { winston.info('[2014/2/19] Updating MOTD to use the HTML widget - skipped'); next(); } }, function(next) { thisSchemaDate = new Date(2014, 1, 20, 15, 30).getTime(); if (schemaDate < thisSchemaDate) { updatesMade = true; var container = '<div class="panel panel-default"><div class="panel-heading">{title}</div><div class="panel-body">{body}</div></div>'; db.setObjectField('widgets:category.tpl', 'sidebar', JSON.stringify([ { "widget": "recentreplies", "data": { "title": "Recent Replies", "container": container } }, { "widget": "activeusers", "data": { "title": "Active Users", "container": container } }, { "widget": "moderators", "data": { "title": "Moderators", "container": container } } ]), function(err) { winston.info('[2014/2/20] Adding Recent Replies, Active Users, and Moderator widgets to category sidebar.'); next(err); }); } else { winston.info('[2014/2/20] Adding Recent Replies, Active Users, and Moderator widgets to category sidebar - skipped'); next(); } }, function(next) { thisSchemaDate = new Date(2014, 1, 20, 16, 15).getTime(); if (schemaDate < thisSchemaDate) { updatesMade = true; db.setObjectField('widgets:home.tpl', 'footer', JSON.stringify([ { "widget": "forumstats", "data": {} } ]), function(err) { winston.info('[2014/2/20] Adding Forum Stats Widget to the Homepage Footer.'); next(err); }); } else { winston.info('[2014/2/20] Adding Forum Stats Widget to the Homepage Footer - skipped'); next(); } }, function(next) { thisSchemaDate = new Date(2014, 1, 20, 19, 45).getTime(); if (schemaDate < thisSchemaDate) { updatesMade = true; var container = '<div class="panel panel-default"><div class="panel-heading">{title}</div><div class="panel-body">{body}</div></div>'; db.setObjectField('widgets:home.tpl', 'sidebar', JSON.stringify([ { "widget": "html", "data": { "html": Meta.config['motd'] || "Welcome to NodeBB, if you are an administrator of this forum visit the <a target='_blank' href='/admin/themes'>Themes</a> ACP to modify and add widgets.", "container": container, "title": "MOTD" } } ]), function(err) { winston.info('[2014/2/20] Updating Lavender MOTD'); next(err); }); } else { winston.info('[2014/2/20] Updating Lavender MOTD - skipped'); next(); } }, function(next) { thisSchemaDate = new Date(2014, 1, 20, 20, 25).getTime(); if (schemaDate < thisSchemaDate) { updatesMade = true; db.setAdd('plugins:active', 'nodebb-widget-essentials', function(err) { winston.info('[2014/2/20] Activating NodeBB Essential Widgets'); Plugins.reload(function() { next(err); }); }); } else { winston.info('[2014/2/20] Activating NodeBB Essential Widgets - skipped'); next(); } }, function(next) { thisSchemaDate = new Date(2014, 1, 22).getTime(); if (schemaDate < thisSchemaDate) { updatesMade = true; db.exists('categories:cid', function(err, exists) { if(err) { return next(err); } if(!exists) { winston.info('[2014/2/22] Added categories to sorted set - skipped'); return next(); } db.getListRange('categories:cid', 0, -1, function(err, cids) { if(err) { return next(err); } if(!Array.isArray(cids)) { winston.info('[2014/2/22] Add categories to sorted set - skipped (cant find any cids)'); return next(); } db.rename('categories:cid', 'categories:cid:old', function(err) { if(err) { return next(err); } async.each(cids, function(cid, next) { Categories.getCategoryField(cid, 'order', function(err, order) { if(err) { return next(err); } db.sortedSetAdd('categories:cid', order, cid, next); }); }, function(err) { if(err) { return next(err); } winston.info('[2014/2/22] Added categories to sorted set'); db.delete('categories:cid:old', next); }); }); }); }); } else { winston.info('[2014/2/22] Added categories to sorted set - skipped'); next(); } } // Add new schema updates here // IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema IN LINE 22!!! ], function(err) { if (!err) { db.set('schemaDate', thisSchemaDate, function(err) { if (!err) { if(updatesMade) { winston.info('[upgrade] Schema update complete!'); } else { winston.info('[upgrade] Schema already up to date!'); } if (callback) { callback(err); } else { process.exit(); } } else { winston.error('[upgrade] Could not update NodeBB schema data!'); process.exit(); } }); } else { switch(err.message) { case 'upgrade-not-possible': winston.error('[upgrade] NodeBB upgrade could not complete, as your database schema is too far out of date.'); winston.error('[upgrade] Please ensure that you did not skip any minor version upgrades.'); winston.error('[upgrade] (e.g. v0.1.x directly to v0.3.x)'); process.exit(); break; default: winston.error('[upgrade] Errors were encountered while updating the NodeBB schema: ' + err.message); process.exit(); break; } } }); }; module.exports = Upgrade;