Merge branch 'master' into v0.5.x

This commit is contained in:
Julian Lam
2014-08-20 18:02:35 -04:00
79 changed files with 942 additions and 641 deletions

View File

@@ -1,5 +1,5 @@
# <img alt="NodeBB" src="http://i.imgur.com/mYxPPtB.png" />
[![Build Status](https://travis-ci.org/NodeBB/NodeBB.svg?branch=master)](https://travis-ci.org/nodebb/nodebb)
[![Build Status](https://travis-ci.org/NodeBB/NodeBB.svg?branch=master)](https://travis-ci.org/NodeBB/NodeBB)
[![Dependency Status](https://david-dm.org/nodebb/nodebb.svg)](https://david-dm.org/nodebb/nodebb)
[![Code Climate](https://codeclimate.com/github/designcreateplay/NodeBB.png)](https://codeclimate.com/github/designcreateplay/NodeBB)

View File

@@ -58,7 +58,7 @@
"validator": "~3.16.1",
"winston": "~0.7.2",
"xregexp": "~2.0.0",
"templates.js": "0.0.8"
"templates.js": "0.0.13"
},
"devDependencies": {
"mocha": "~1.13.0"
@@ -67,7 +67,7 @@
"url": "https://github.com/NodeBB/NodeBB/issues"
},
"engines": {
"node": ">=0.8"
"node": ">=0.10"
},
"maintainers": [
{

View File

@@ -13,7 +13,7 @@
"chat.pop-out": "Pop out chat",
"chat.maximize": "Maximize",
"composer.user_said_in": "%1 said in %2:\n",
"composer.user_said": "%1 said:\n",
"composer.user_said_in": "%1 said in %2:",
"composer.user_said": "%1 said:",
"composer.discard": "Are you sure you wish to discard this post?"
}

View File

@@ -5,6 +5,7 @@
"recent": "Recent Topics",
"users": "Registered Users",
"notifications": "Notifications",
"tags": "Topics tagged under \"%1\"",
"user.edit": "Editing \"%1\"",
"user.following": "People %1 Follows",
"user.followers": "People who Follow %1",

View File

@@ -105,6 +105,7 @@
"more_users_and_guests": "%1 more user(s) and %2 guest(s)",
"more_users": "%1 more user(s)",
"more_guests": "%1 more guest(s)",
"users_and_others": "%1 and %2 others",
"sort_by": "Sort by",
"oldest_to_newest": "Oldest to Newest",

View File

@@ -50,9 +50,9 @@
"read_more": "loe veel",
"posted_ago_by_guest": "postitatud %1 tagasi külalise poolt",
"posted_ago_by": "postitatud %1 tagasi kasutaja %2 poolt",
"posted_ago": "postitatud %1 tagasi",
"posted_ago": "postitatud %1",
"posted_in_ago_by_guest": "postitatud kategooriasse %1 %2 tagasi külalise poolt",
"posted_in_ago_by": "postitatud kategooriasse %1 %2 aega tagasi kasutaja %3 poolt",
"posted_in_ago_by": "postitatud kategooriasse %1 %2 kasutaja %3 poolt",
"posted_in_ago": "postitatud kategooriasse %1 %2 tagasi",
"replied_ago": "vastas %1",
"user_posted_ago": "kasutaja %1 postitas %2 tagasi",

View File

@@ -41,7 +41,7 @@
"thread_tools.pin": "Tõsta esile teema",
"thread_tools.unpin": "Märgista teema",
"thread_tools.lock": "Lukusta teema",
"thread_tools.unlock": "Eemalda märgistatud teema",
"thread_tools.unlock": "Taasava teema",
"thread_tools.move": "Liiguta teema",
"thread_tools.move_all": "Liiguta kõik",
"thread_tools.fork": "Fork Topic",

View File

@@ -1,20 +1,20 @@
{
"password-reset-requested": "Password Reset Requested - %1!",
"welcome-to": "Welcome to %1",
"greeting_no_name": "Hello",
"greeting_with_name": "Hello %1",
"welcome.text1": "Thank you for registering with %1!",
"password-reset-requested": "Recuperação de Senha Solicitada - %1!",
"welcome-to": "Bem vindo a %1",
"greeting_no_name": "Olá",
"greeting_with_name": "Olà %1",
"welcome.text1": "Obrigado por se registrar com %1!",
"welcome.text2": "To fully activate your account, we need to verify that you own the email address you registered with.",
"welcome.cta": "Click here to confirm your email address",
"welcome.cta": "Clique aqui para confirmar seu endereço de email",
"reset.text1": "We received a request to reset your password, possibly because you have forgotten it. If this is not the case, please ignore this email.",
"reset.text2": "To continue with the password reset, please click on the following link:",
"reset.cta": "Click here to reset your password",
"reset.cta": "Clique aqui para recuperar sua senha",
"digest.notifications": "You have some unread notifications from %1:",
"digest.latest_topics": "Latest topics from %1",
"digest.cta": "Click here to visit %1",
"digest.latest_topics": "Últimos tópicos de %1",
"digest.cta": "Clique aqui para visitar %1",
"digest.unsub.info": "This digest was sent to you due to your subscription settings.",
"digest.unsub.cta": "Click here to alter those settings",
"digest.unsub.cta": "Clique aqui para alterar suas configurações",
"digest.daily.no_topics": "There have been no active topics in the past day",
"test.text1": "This is a test email to verify that the emailer is set up correctly for your NodeBB.",
"closing": "Thanks!"
"closing": "Obrigado!"
}

View File

@@ -25,7 +25,7 @@
"no-user": "Usuário não existe",
"no-teaser": "Chamada não existe",
"no-privileges": "Você não possui permissões para esta ação.",
"no-emailers-configured": "No email plugins were loaded, so a test email could not be sent",
"no-emailers-configured": "Nenhum plugin de email foi carregado, por isso o email de teste não pode ser enviado",
"category-disabled": "Categoria desativada",
"topic-locked": "Tópico trancado",
"still-uploading": "Aguarde a conclusão dos uploads.",

View File

@@ -4,9 +4,9 @@
"see_all": "Visualizar todas as notificações",
"back_to_home": "Voltar para %1",
"outgoing_link": "Link Externo",
"outgoing_link_message": "You are now leaving %1.",
"continue_to": "Continue to %1",
"return_to": "Return to %1",
"outgoing_link_message": "Você deixou de seguir %1.",
"continue_to": "Continuar para %1",
"return_to": "Voltar para %1",
"new_notification": "Nova notificação",
"you_have_unread_notifications": "Você possui notificações não lidas.",
"new_message_from": "Nova mensagem de <strong>%1</strong>",

View File

@@ -1,20 +1,20 @@
{
"password-reset-requested": "Password Reset Requested - %1!",
"welcome-to": "Welcome to %1",
"greeting_no_name": "Hello",
"greeting_with_name": "Hello %1",
"welcome.text1": "Thank you for registering with %1!",
"welcome.text2": "To fully activate your account, we need to verify that you own the email address you registered with.",
"welcome.cta": "Click here to confirm your email address",
"reset.text1": "We received a request to reset your password, possibly because you have forgotten it. If this is not the case, please ignore this email.",
"reset.text2": "To continue with the password reset, please click on the following link:",
"reset.cta": "Click here to reset your password",
"digest.notifications": "You have some unread notifications from %1:",
"digest.latest_topics": "Latest topics from %1",
"digest.cta": "Click here to visit %1",
"digest.unsub.info": "This digest was sent to you due to your subscription settings.",
"digest.unsub.cta": "Click here to alter those settings",
"digest.daily.no_topics": "There have been no active topics in the past day",
"test.text1": "This is a test email to verify that the emailer is set up correctly for your NodeBB.",
"closing": "Thanks!"
"password-reset-requested": "Пароль сброшен - %1!",
"welcome-to": "Добро пожаловать на %1",
"greeting_no_name": "Здравствуйте!",
"greeting_with_name": "Здравствуйте, %1!",
"welcome.text1": "Благодарим за регистрацию %1! ",
"welcome.text2": "Для активации вашей учетной записи мы должны убедиться, что вы указали верный email адрес.",
"welcome.cta": "Перейдите по ссылке для подтверждения вашего email",
"reset.text1": "Мы получили запрос на изменение вашего пароля. Если не подавали запрос на изменение пароля, пожалуйста, проигнорируйте это сообщение.",
"reset.text2": "Для продолжения процедуры изменения пароля, пожалуйста, перейдите по ссылке:",
"reset.cta": "Кликните здесь для изменения пароля",
"digest.notifications": "У вас есть непрочитанные уведомления от %1:",
"digest.latest_topics": "Последние темы %1",
"digest.cta": "Кликните здесь для просмотра %1",
"digest.unsub.info": "Вам была выслана сводка новостей в соответствии с вашими настройками.",
"digest.unsub.cta": "Кликните здесь для изменения ваших настроек.",
"digest.daily.no_topics": "За минувший день новых тем нет.",
"test.text1": "Это тестовое сообщение для проверки почтового сервиса NodeBB.",
"closing": "Спасибо!"
}

View File

@@ -15,7 +15,7 @@
"invalid-pagination-value": "Неверное значение пагинации",
"username-taken": "Имя пользователя занято",
"email-taken": "Email занят",
"email-not-confirmed": "Your email is not confirmed, please click here to confirm your email.",
"email-not-confirmed": "Ваш email не подтвержден, нажмите для подтверждения.",
"username-too-short": "Слишком короткое имя пользователя",
"user-banned": "Пользователь заблокирован",
"no-category": "Несуществующая категория",
@@ -25,10 +25,10 @@
"no-user": "Несуществующий пользователь",
"no-teaser": "Несуществующее превью",
"no-privileges": "У вас недостаточно прав, чтобы совершить данное действие.",
"no-emailers-configured": "No email plugins were loaded, so a test email could not be sent",
"no-emailers-configured": "Не подключен ни один плагин для отправки почты, поэтому тестовый email не может быть отправлен",
"category-disabled": "Категория отключена",
"topic-locked": "Тема закрыт",
"still-uploading": "Пожалуйста, подождите завершения загрузки",
"topic-locked": "Тема закрыта",
"still-uploading": "Пожалуйста, подождите завершения загрузки.",
"content-too-short": "Пост должен содержать минимум %1 символов.",
"title-too-short": "Заголовок должен содержать минимум %1 символов.",
"title-too-long": "Заголовок не может быть длиннее %1 символов.",

View File

@@ -13,7 +13,7 @@
"please_log_in": "Пожалуйста, войдите под своим аккаунтом",
"logout": "Выйти",
"posting_restriction_info": "Сообщения могут оставлять только зарегистрированные пользователи, нажмите сюда, чтобы войти",
"welcome_back": "Welcome Back",
"welcome_back": "С возвращением",
"you_have_successfully_logged_in": "Вы вышли из аккаунта",
"save_changes": "Сохранить изменения",
"close": "Закрыть",

View File

@@ -1,7 +1,7 @@
{
"view_group": "View Group",
"details.title": "Group Details",
"details.members": "Member List",
"details.has_no_posts": "This group's members have not made any posts.",
"details.latest_posts": "Latest Posts"
"view_group": "Просмотр группы",
"details.title": "Информация о группе",
"details.members": "Список пользователей",
"details.has_no_posts": "Пользователями этой группы не публиковали никаких записей",
"details.latest_posts": "Последние записи"
}

View File

@@ -1,18 +1,18 @@
{
"chat.chatting_with": "Чат с <span id=\"chat-with-name\"></span>",
"chat.placeholder": "Type chat message here, press enter to send",
"chat.placeholder": "Введите сообщение, нажмите ENTER для отправки",
"chat.send": "Отправить",
"chat.no_active": "У вас нет активных чатов.",
"chat.user_typing": "%1 печатает ...",
"chat.user_has_messaged_you": "%1 отправил вам сообщение.",
"chat.see_all": "Просмотр всех диалогов",
"chat.no-messages": "Please select a recipient to view chat message history",
"chat.recent-chats": "Recent Chats",
"chat.contacts": "Contacts",
"chat.message-history": "Message History",
"chat.pop-out": "Pop out chat",
"chat.maximize": "Maximize",
"composer.user_said_in": "%1 said in %2:",
"composer.user_said": "%1 said:",
"chat.no-messages": "Пожалуйста, выберите собеседника для просмотра истории сообщений",
"chat.recent-chats": "Последние переписки",
"chat.contacts": "Контакты",
"chat.message-history": "История сообщений",
"chat.pop-out": "Покинуть диалог",
"chat.maximize": "Развернуть",
"composer.user_said_in": "%1 сказал %2:",
"composer.user_said": "%1 сказал:",
"composer.discard": "Вы уверены, что хотите отказаться от этого поста?"
}

View File

@@ -4,9 +4,9 @@
"see_all": "Просмотреть все уведомления",
"back_to_home": "Назад к %1",
"outgoing_link": "Внешняя ссылка",
"outgoing_link_message": "You are now leaving %1.",
"continue_to": "Continue to %1",
"return_to": "Return to %1",
"outgoing_link_message": "Вы покидаете %1.",
"continue_to": "Перейти на %1",
"return_to": "Вернуться к %1",
"new_notification": "Новое Уведомление",
"you_have_unread_notifications": "У вас есть непрочитанные уведомления",
"new_message_from": "Новое сообщение от <strong>%1</strong>",

View File

@@ -3,6 +3,6 @@
"day": "День",
"week": "Неделя",
"month": "Месяц",
"year": "Year",
"year": "Год",
"no_recent_topics": "Нет свежих тем."
}

View File

@@ -1,3 +1,3 @@
{
"results_matching": "%1 result(s) matching \"%2\", (%3 seconds)"
"results_matching": "%1 результатов по фразе \"%2\", (%3 секунды) "
}

View File

@@ -54,7 +54,7 @@
"topic_move_success": "Эта тема успешно перемещена в %1",
"post_delete_confirm": "Вы уверены, что хотите удалить этот пост?",
"post_restore_confirm": "Вы уверены, что хотите восстановить этот пост?",
"post_purge_confirm": "Are you sure you want to purge this post?",
"post_purge_confirm": "Вы уверены, что хотите очистить эту запись?",
"load_categories": "Загружаем Категории",
"disabled_categories_note": "Отключенные категории затемненны",
"confirm_move": "Перенести",

View File

@@ -3,7 +3,7 @@
"offline": "Не в сети",
"username": "Имя пользователя",
"email": "Email",
"confirm_email": "Confirm Email",
"confirm_email": "Подтвердить Email",
"fullname": "Полное имя",
"website": "Сайт",
"location": "Откуда",

View File

@@ -6,5 +6,5 @@
"enter_username": "Введите имя пользователя для поиска",
"load_more": "Загрузить еще",
"user-not-found": "Пользователь не найден!",
"users-found-search-took": "Нашел %1 пользователя(ей)! Поиск занял %2 ms."
"users-found-search-took": "Нашел %1 пользователя(ей)! Поиск занял %2 мс."
}

View File

@@ -1,20 +1,20 @@
{
"password-reset-requested": "Password Reset Requested - %1!",
"welcome-to": "Welcome to %1",
"greeting_no_name": "Hello",
"greeting_with_name": "Hello %1",
"welcome.text1": "Thank you for registering with %1!",
"welcome.text2": "To fully activate your account, we need to verify that you own the email address you registered with.",
"welcome.cta": "Click here to confirm your email address",
"reset.text1": "We received a request to reset your password, possibly because you have forgotten it. If this is not the case, please ignore this email.",
"reset.text2": "To continue with the password reset, please click on the following link:",
"reset.cta": "Click here to reset your password",
"digest.notifications": "You have some unread notifications from %1:",
"digest.latest_topics": "Latest topics from %1",
"digest.cta": "Click here to visit %1",
"digest.unsub.info": "This digest was sent to you due to your subscription settings.",
"digest.unsub.cta": "Click here to alter those settings",
"digest.daily.no_topics": "There have been no active topics in the past day",
"test.text1": "This is a test email to verify that the emailer is set up correctly for your NodeBB.",
"closing": "Thanks!"
"password-reset-requested": "Parola Değiştirme İsteği Gönderildi",
"welcome-to": "Hoşgeldiniz",
"greeting_no_name": "Merhaba",
"greeting_with_name": "Merhaba %1",
"welcome.text1": "Kaydolduğunuz için teşekkürler!",
"welcome.text2": "Hesabınızı aktif hale getirmek için, kaydolduğunuz e-posta adresinin size ait olduğunu onaylamamız gerekiyor.",
"welcome.cta": "E-posta adresinizi onaylamak için buraya tıklayın",
"reset.text1": "Şifrenizi değiştirmek istediğinize dair bir ileti aldık. Eğer böyle bir istek göndermediyseniz, lütfen bu e-postayı görmezden gelin.",
"reset.text2": "Parola değiştirme işlemine devam etmek için aşağıdaki bağlantıya tıklayın:",
"reset.cta": "Parolanızı değiştirmek için buraya tıklayın",
"digest.notifications": "Okunmamış bazı bildirimleriniz var",
"digest.latest_topics": "En güncel konular",
"digest.cta": "Ziyaret etmek için buraya tıklayın",
"digest.unsub.info": "Bu e-posta seçtiğiniz ayarlar nedeniyle gönderildi.",
"digest.unsub.cta": "Bu ayarları değiştirmek için buraya tıklayın",
"digest.daily.no_topics": "Geçtiğimiz gün içinde aktif bir konu yok.",
"test.text1": "Bu ileti NodeBB e-posta ayarlarınızın doğru çalışıp çalışmadığını kontrol etmek için gönderildi.",
"closing": "Teşekkürler!"
}

View File

@@ -1,7 +1,7 @@
{
"view_group": "View Group",
"details.title": "Group Details",
"details.members": "Member List",
"details.has_no_posts": "This group's members have not made any posts.",
"details.latest_posts": "Latest Posts"
"view_group": "Grubu Gör",
"details.title": "Grup Detayları",
"details.members": "Üye Listesi",
"details.has_no_posts": "Bu grubun üyeleri henüz bir ileti göndermedi.",
"details.latest_posts": "En son iletiler"
}

View File

@@ -1,3 +1,3 @@
{
"results_matching": "%1 result(s) matching \"%2\", (%3 seconds)"
"results_matching": "%1 tane “%2“ bulundu (%3 saniye)"
}

View File

@@ -1,20 +1,20 @@
{
"password-reset-requested": "Password Reset Requested - %1!",
"welcome-to": "Welcome to %1",
"greeting_no_name": "Hello",
"greeting_with_name": "Hello %1",
"welcome.text1": "Thank you for registering with %1!",
"welcome.text2": "To fully activate your account, we need to verify that you own the email address you registered with.",
"welcome.cta": "Click here to confirm your email address",
"reset.text1": "We received a request to reset your password, possibly because you have forgotten it. If this is not the case, please ignore this email.",
"reset.text2": "To continue with the password reset, please click on the following link:",
"reset.cta": "Click here to reset your password",
"digest.notifications": "You have some unread notifications from %1:",
"digest.latest_topics": "Latest topics from %1",
"digest.cta": "Click here to visit %1",
"digest.unsub.info": "This digest was sent to you due to your subscription settings.",
"digest.unsub.cta": "Click here to alter those settings",
"digest.daily.no_topics": "There have been no active topics in the past day",
"test.text1": "This is a test email to verify that the emailer is set up correctly for your NodeBB.",
"closing": "Thanks!"
"password-reset-requested": "密码重设申请 - %1!",
"welcome-to": "欢迎来到 %1",
"greeting_no_name": "您好",
"greeting_with_name": "%1您好",
"welcome.text1": "谢谢您使用 %1 注册帐户!",
"welcome.text2": "如需完全激活您的帐户,我们需要校验您注册的电子邮箱地址。",
"welcome.cta": "点击这里确认您的电子邮箱地址",
"reset.text1": "我们收到了重置您密码的申请,可能因为您忘记了密码。如果不是,请忽略这封邮件。",
"reset.text2": "如需继续重置密码,请点击下面的链接:",
"reset.cta": "点击这里重设您的密码",
"digest.notifications": "您有一些来自 %1 的未读通知:",
"digest.latest_topics": "来自 %1 的最新主题",
"digest.cta": "点击这里访问 %1",
"digest.unsub.info": "根据您的订阅设置,为您发送此摘要。",
"digest.unsub.cta": "点击这里修改这些设置",
"digest.daily.no_topics": "最近几天有一些未激活的主题",
"test.text1": "这是一封测试邮件,用来验证 NodeBB 的邮件配置是否设置正确。",
"closing": "谢谢!"
}

View File

@@ -1,7 +1,7 @@
{
"view_group": "View Group",
"details.title": "Group Details",
"details.members": "Member List",
"details.has_no_posts": "This group's members have not made any posts.",
"details.latest_posts": "Latest Posts"
"view_group": "查看用户组",
"details.title": "用户组详情",
"details.members": "会员列表",
"details.has_no_posts": "此用户组的会员尚未发表任何帖子。",
"details.latest_posts": "最新帖子"
}

View File

@@ -1,3 +1,3 @@
{
"results_matching": "%1 result(s) matching \"%2\", (%3 seconds)"
"results_matching": "%1 条结果,匹配 \"%2\"(耗时 %3 秒)"
}

View File

@@ -96,12 +96,13 @@ var ajaxify = ajaxify || {};
translator.translate(template, function(translatedTemplate) {
setTimeout(function() {
$('#content').html(translatedTemplate);
ajaxify.variables.parse();
ajaxify.widgets.render(tpl_url, url, function() {
$(window).trigger('action:ajaxify.end', {url: url});
});
ajaxify.variables.parse();
$(window).trigger('action:ajaxify.contentLoaded', {url: url});
ajaxify.loadScript(tpl_url);
@@ -203,7 +204,6 @@ var ajaxify = ajaxify || {};
}
var location = document.location || window.location,
api_url = (url === '' || url === '/') ? 'home' : url,
tpl_url = ajaxify.getCustomTemplateMapping(url.split('?')[0]);
if (!tpl_url) {
@@ -211,7 +211,7 @@ var ajaxify = ajaxify || {};
}
apiXHR = $.ajax({
url: RELATIVE_PATH + '/api/' + api_url,
url: RELATIVE_PATH + '/api/' + url,
cache: false,
success: function(data) {
if (!data) {
@@ -329,9 +329,9 @@ var ajaxify = ajaxify || {};
templates.registerLoader(ajaxify.loadTemplate);
$.when($.getJSON(RELATIVE_PATH + '/templates/config.json'), $.getJSON(RELATIVE_PATH + '/api/get_templates_listing')).done(function (config_data, templates_data) {
templatesConfig = config_data[0];
availableTemplates = templates_data[0];
$.getJSON(RELATIVE_PATH + '/api/get_templates_listing', function (data) {
templatesConfig = data.templatesConfig;
availableTemplates = data.availableTemplates;
app.load();
});

View File

@@ -413,8 +413,8 @@ var socket,
if (utils.findBootstrapEnvironment() === 'xs') {
return;
}
$('#header-menu li i[title]').each(function() {
$(this).parents('a').tooltip({
$('#header-menu li [title]').each(function() {
$(this).tooltip({
placement: 'bottom',
title: $(this).attr('title')
});
@@ -441,7 +441,7 @@ var socket,
searchButton.show();
}
searchButton.off().on('click', function(e) {
searchButton.on('click', function(e) {
if (!config.loggedIn && !config.allowGuestSearching) {
app.alert({
message:'[[error:search-requires-login]]',

View File

@@ -1,5 +1,5 @@
"use strict";
/*global define, socket, app, bootbox, tabIndent, config, RELATIVE_PATH*/
/*global define, socket, app, bootbox, tabIndent, config, RELATIVE_PATH, templates */
define('forum/admin/themes', ['forum/admin/settings'], function(Settings) {
var Themes = {};
@@ -71,17 +71,17 @@ define('forum/admin/themes', ['forum/admin/settings'], function(Settings) {
if (confirm) {
socket.emit('admin.themes.set', {
type: 'local',
id: 'nodebb-theme-cerulean'
id: 'nodebb-theme-vanilla'
}, function(err) {
if (err) {
return app.alertError(err.message);
}
highlightSelectedTheme('nodebb-theme-cerulean');
highlightSelectedTheme('nodebb-theme-vanilla');
app.alert({
alert_id: 'admin:theme',
type: 'success',
title: 'Theme Changed',
message: 'You have successfully reverted your NodeBB back to it\'s default theme. Restarting your NodeBB <i class="fa fa-refresh fa-spin"></i>',
message: 'You have successfully reverted your NodeBB back to it\'s default theme.',
timeout: 3500
});
});
@@ -95,34 +95,19 @@ define('forum/admin/themes', ['forum/admin/settings'], function(Settings) {
return app.alertError(err.message);
}
var instListEl = $('#installed_themes').empty(), liEl;
var instListEl = $('#installed_themes');
if (!themes.length) {
instListEl.append($('<li/ >').addClass('no-themes').html('No installed themes found'));
return;
} else {
templates.parse('partials/admin/theme_list', {
themes: themes
}, function(html) {
instListEl.html(html);
highlightSelectedTheme(config['theme:id']);
});
}
for (var x = 0, numThemes = themes.length; x < numThemes; x++) {
liEl = $('<li/ >').attr({
'data-type': 'local',
'data-theme': themes[x].id
}).html('<img src="' + (themes[x].screenshot ? RELATIVE_PATH + '/css/previews/' + themes[x].id : RELATIVE_PATH + '/images/themes/default.png') + '" />' +
'<div>' +
'<div class="pull-right">' +
'<button class="btn btn-primary" data-action="use">Use</button> ' +
'</div>' +
'<h4>' + themes[x].name + '</h4>' +
'<p>' +
themes[x].description +
(themes[x].url ? ' (<a href="' + themes[x].url + '">Homepage</a>)' : '') +
'</p>' +
'</div>' +
'<div class="clear">');
instListEl.append(liEl);
}
highlightSelectedTheme(config['theme:id']);
});
// Proper tabbing for "Custom CSS" field
@@ -138,26 +123,23 @@ define('forum/admin/themes', ['forum/admin/settings'], function(Settings) {
};
Themes.render = function(bootswatch) {
var themeContainer = $('#bootstrap_themes').empty(),
numThemes = bootswatch.themes.length, themeEl, theme;
var themeContainer = $('#bootstrap_themes');
for (var x = 0; x < numThemes; x++) {
theme = bootswatch.themes[x];
themeEl = $('<li />').attr({
'data-type': 'bootswatch',
'data-css': theme.cssCdn,
'data-theme': theme.name
}).html('<img src="' + theme.thumbnail + '" />' +
'<div>' +
'<div class="pull-right">' +
'<button class="btn btn-primary" data-action="use">Use</button> ' +
'</div>' +
'<h4>' + theme.name + '</h4>' +
'<p>' + theme.description + '</p>' +
'</div>' +
'<div class="clear">');
themeContainer.append(themeEl);
}
templates.parse('partials/admin/theme_list', {
themes: bootswatch.themes.map(function(theme) {
return {
type: 'bootswatch',
id: theme.name,
name: theme.name,
description: theme.description,
screenshot_url: theme.thumbnail,
url: theme.preview,
css: theme.cssCdn
};
})
}, function(html) {
themeContainer.html(html);
});
};
Themes.prepareWidgets = function() {

View File

@@ -28,7 +28,7 @@ define('forum/login', function() {
if (previousUrl) {
app.previousUrl = previousUrl;
} else if (!app.previousUrl) {
app.previousUrl = '/';
app.previousUrl = RELATIVE_PATH || '/';
}
if(app.previousUrl.indexOf('/reset/') !== -1) {

View File

@@ -72,13 +72,11 @@ define('forum/topic', dependencies, function(pagination, infinitescroll, threadT
$(window).trigger('action:topic.loaded');
socket.emit('topics.markAsRead', tid);
socket.emit('topics.markTopicNotificationsRead', tid);
socket.emit('topics.increaseViewCount', tid);
socket.emit('topics.enter', tid);
};
Topic.toTop = function() {
navigator.scrollTop(1);
navigator.scrollTop(0);
};
Topic.toBottom = function() {
@@ -353,7 +351,10 @@ define('forum/topic', dependencies, function(pagination, infinitescroll, threadT
utils.makeNumbersHumanReadable(element.find('.human-readable-number'));
element.find('span.timeago').timeago();
element.find('.post-content img:not(.emoji)').addClass('img-responsive').each(function() {
$(this).wrap('<a href="' + $(this).attr('src') + '" target="_blank">');
var $this = $(this);
if (!$this.parent().is('a')) {
$this.wrap('<a href="' + $this.attr('src') + '" target="_blank">');
}
});
postTools.updatePostCount();
showBottomPostBar();
@@ -364,7 +365,7 @@ define('forum/topic', dependencies, function(pagination, infinitescroll, threadT
postHtml.find('.move').toggleClass('none', !privileges.move);
postHtml.find('.reply, .quote').toggleClass('none', !$('.post_reply').length);
var isSelfPost = parseInt(postHtml.attr('data-uid'), 10) === parseInt(app.uid, 10);
postHtml.find('.chat, .flag').toggleClass('none', isSelfPost);
postHtml.find('.chat, .flag').toggleClass('none', isSelfPost || !app.uid);
}
function loadMorePosts(direction) {

View File

@@ -36,13 +36,26 @@ define('forum/topic/postTools', ['composer', 'share', 'navigator'], function(com
};
function addFavouriteHandler() {
$('#post-container').on('mouseenter', '.favourite-tooltip', function(e) {
$('#post-container').on('mouseenter', '.favourite-tooltip', function() {
if (!$(this).data('users-loaded')) {
$(this).data('users-loaded', "true");
$(this).data('users-loaded', 'true');
var pid = $(this).parents('.post-row').attr('data-pid');
var el = $(this).attr('title', "Loading...");
socket.emit('posts.getFavouritedUsers', pid, function(err, usernames) {
el.attr('title', usernames).tooltip('show');
if (err) {
return;
}
if (usernames.length > 6) {
var otherCount = usernames.length - 5;
usernames = usernames.slice(0, 5).join(', ').replace(/,/g, '|');
translator.translate('[[topic:users_and_others, ' + usernames + ', ' + otherCount + ']]', function(translated) {
translated = translated.replace(/\|/g, ',');
el.attr('title', translated).tooltip('show');
});
} else {
usernames = usernames.join(', ');
el.attr('title', usernames).tooltip('show');
}
});
}
});
@@ -137,7 +150,7 @@ define('forum/topic/postTools', ['composer', 'share', 'navigator'], function(com
if($('.composer').length) {
composer.addQuote(tid, ajaxify.variables.get('topic_slug'), getData(button, 'data-index'), pid, topicName, username, quoted);
} else {
composer.newReply(tid, pid, topicName, '[[modules:composer.user_said, ' + username + ']]' + quoted);
composer.newReply(tid, pid, topicName, '[[modules:composer.user_said, ' + username + ']]\n' + quoted);
}
});
}

View File

@@ -14,7 +14,7 @@
property = tag.property ? 'property="' + tag.property + '" ' : '',
content = tag.content ? 'content="' + tag.content.replace(/\n/g, ' ') + '" ' : '';
return '<meta ' + name + property + content + ' />';
return '<meta ' + name + property + content + '/>';
};
if ('undefined' !== typeof window) {

View File

@@ -249,7 +249,7 @@ define('chat', ['taskbar', 'string', 'sounds', 'forum/chats'], function(taskbar,
module.center = function(chatModal) {
chatModal.css("left", Math.max(0, (($(window).width() - $(chatModal).outerWidth()) / 2) + $(window).scrollLeft()) + "px");
chatModal.css("top", $(window).height() / 4 - $(chatModal).outerHeight() / 2);
chatModal.css("top", Math.max(0, $(window).height() / 4 - $(chatModal).outerHeight() / 2));
chatModal.css("zIndex", 2000);
chatModal.find('#chat-message-input').focus();
return chatModal;

View File

@@ -119,7 +119,7 @@ define('composer', dependencies, function(taskbar, controls, uploads, formatting
var uuid = composer.active;
if (uuid === undefined) {
composer.newReply(tid, pid, title, '[[modules:composer.user_said, ' + username + ']]' + text);
composer.newReply(tid, pid, title, '[[modules:composer.user_said, ' + username + ']]\n' + text);
return;
}
var postContainer = $('#cmp-uuid-' + uuid);
@@ -127,9 +127,9 @@ define('composer', dependencies, function(taskbar, controls, uploads, formatting
var prevText = bodyEl.val();
if (parseInt(tid, 10) !== parseInt(composer.posts[uuid].tid, 10)) {
var link = '[' + title + '](/topic/' + topicSlug + '/' + (parseInt(postIndex, 10) + 1) + ')';
translator.translate('[[modules:composer.user_said_in, ' + username + ', ' + link + ']]', onTranslated);
translator.translate('[[modules:composer.user_said_in, ' + username + ', ' + link + ']]\n', onTranslated);
} else {
translator.translate('[[modules:composer.user_said, ' + username + ']]', onTranslated);
translator.translate('[[modules:composer.user_said, ' + username + ']]\n', onTranslated);
}
function onTranslated(translated) {
@@ -360,33 +360,45 @@ define('composer', dependencies, function(taskbar, controls, uploads, formatting
return composerAlert('[[error:content-too-short, ' + config.minimumPostLength + ']]');
}
var composerData = {}, action;
if (parseInt(postData.cid, 10) > 0) {
socket.emit('topics.post', {
composerData = {
title: titleEl.val(),
content: bodyEl.val(),
topic_thumb: thumbEl.val() || '',
category_id: postData.cid,
tags: tags.getTags(post_uuid)
}, function(err, topic) {
};
action = 'topics.post';
socket.emit(action, composerData, function(err, topic) {
done(err);
if (!err) {
ajaxify.go('topic/' + topic.slug);
}
});
} else if (parseInt(postData.tid, 10) > 0) {
socket.emit('posts.reply', {
composerData = {
tid: postData.tid,
content: bodyEl.val(),
toPid: postData.toPid
}, done);
};
action = 'posts.reply';
socket.emit(action, composerData, done);
} else if (parseInt(postData.pid, 10) > 0) {
socket.emit('posts.edit', {
composerData = {
pid: postData.pid,
content: bodyEl.val(),
title: titleEl.val(),
topic_thumb: thumbEl.val() || '',
tags: tags.getTags(post_uuid)
}, done);
};
action = 'posts.edit';
socket.emit(action, composerData, done);
}
function done(err) {
@@ -401,6 +413,8 @@ define('composer', dependencies, function(taskbar, controls, uploads, formatting
discard(post_uuid);
drafts.removeDraft(postData.save_id);
$(window).trigger('action:composer.' + action, composerData);
}
}

View File

@@ -110,7 +110,7 @@ define('navigator', ['forum/pagination'], function(pagination) {
navigator.scrollTop = function(index) {
if ($('li[data-index="' + index + '"]').length) {
navigator.scrollUp();
navigator.scrollToPost(index, true);
} else {
ajaxify.go(generateUrl());
}
@@ -118,7 +118,7 @@ define('navigator', ['forum/pagination'], function(pagination) {
navigator.scrollBottom = function(index) {
if ($('li[data-index="' + index + '"]').length) {
navigator.scrollDown();
navigator.scrollToPost(index, true);
} else {
index = parseInt(index, 10) + 1;
ajaxify.go(generateUrl(index));

View File

@@ -36,7 +36,7 @@ Please use the npm module instead - require('templates.js')
}
callback(parse(loaded, obj, bind));
});
});
} else {
callback(parse(templates.cache[template], obj, bind));
}
@@ -58,14 +58,15 @@ Please use the npm module instead - require('templates.js')
};
templates.getBlock = function(template, block) {
return template.replace(new RegExp('[\\s\\S]*(<!--[\\s]*BEGIN ' + block + '[\\s]*-->[\r\n]*[\\s\\S]*?[\r\n]*<!--[\\s]*END ' + block + '[\\s]*-->)[\\s\\S]*', 'g'), '$1');
return template.replace(new RegExp('[\\s\\S]*(<!--[\\s]*BEGIN ' + block + '[\\s]*-->[\\s\\S]*?<!--[\\s]*END ' + block + '[\\s]*-->)[\\s\\S]*', 'g'), '$1');
};
function express(filename, options, fn) {
console.log(filename, options, fn);
var fs = require('fs'),
tpl = filename.replace(options.settings.views + '/', '');
options['_locals'] = null;
if (!templates.cache[tpl]) {
fs.readFile(filename, function(err, html) {
templates.cache[tpl] = (html || '').toString();
@@ -82,11 +83,11 @@ Please use the npm module instead - require('templates.js')
}
function makeRegex(block) {
return new RegExp('<!--[\\s]*BEGIN ' + block + '[\\s]*-->[\\s\\S]*?<!--[\\s]*END ' + block + '[\\s]*-->');
return new RegExp('[\\t ]*<!--[\\s]*BEGIN ' + block + '[\\s]*-->[\\s\\S]*?<!--[\\s]*END ' + block + '[\\s]*-->');
}
function makeBlockRegex(block) {
return new RegExp('([\\n]?<!--[\\s]*BEGIN ' + block + '[\\s]*-->[\\n]?)|([\\n]?<!--[\\s]*END ' + block + '[\\s]*-->[\\n]?)', 'g');
return new RegExp('([\\t ]*<!--[\\s]*BEGIN ' + block + '[\\s]*-->[\\r\\n?|\\n]?)|(<!--[\\s]*END ' + block + '[\\s]*-->)', 'g');
}
function makeConditionalRegex(block) {
@@ -94,7 +95,7 @@ Please use the npm module instead - require('templates.js')
}
function makeStatementRegex(key) {
return new RegExp('([\\s]*<!--[\\s]*IF ' + key + '[\\s]*-->)|(<!--[\\s]*ENDIF ' + key + '[\\s]*-->[\\s]*)', 'gi');
return new RegExp('(<!--[\\s]*IF ' + key + '[\\s]*-->)|(<!--[\\s]*ENDIF ' + key + '[\\s]*-->)', 'g');
}
function registerGlobals(obj) {
@@ -113,23 +114,23 @@ Please use the npm module instead - require('templates.js')
if (matches !== null) {
for (var i = 0, ii = matches.length; i < ii; i++) {
var statement = makeStatementRegex(key),
nestedConditionals = matches[i].match(/[\s|\S]<!-- IF[\s\S]*ENDIF[\s\S]*-->[\s|\S]/),
match = matches[i].replace(statement, '').replace(/[\s|\S]<!-- IF[\s\S]*ENDIF[\s\S]*-->[\s|\S]/gi, '<!-- NESTED -->'),
conditionalBlock = match.split(/\s*<!-- ELSE -->\s*/);
nestedConditionals = matches[i].match(/(?!^)<!-- IF([\s\S]*?)ENDIF[ a-zA-Z0-9\._:]*-->(?!$)/gi),
match = matches[i].replace(statement, '').replace(/(?!^)<!-- IF([\s\S]*?)ENDIF[ a-zA-Z0-9\._:]*-->(?!$)/gi, '<!-- NESTED -->'),
conditionalBlock = match.split(/[\r\n?\n]*?<!-- ELSE -->[\r\n?\n]*?/);
if (conditionalBlock[1]) {
// there is an else statement
if (!value) {
template = template.replace(matches[i], conditionalBlock[1].replace(/(^[\r\n\t]*)|([\r\n\t]*$)/gi, ''));
if (!value) { // todo check second line break conditional, doesn't match.
template = template.replace(matches[i], conditionalBlock[1].replace(/(^[\r\n?|\n]*)|([\r\n\t]*$)/gi, ''));
} else {
template = template.replace(matches[i], conditionalBlock[0].replace(/(^[\r\n\t]*)|([\r\n\t]*$)/gi, ''));
template = template.replace(matches[i], conditionalBlock[0].replace(/(^[\r\n?|\n]*)|([\r\n\t]*$)/gi, ''));
}
} else {
// regular if statement
if (!value) {
template = template.replace(matches[i], '');
} else {
template = template.replace(matches[i], match.replace(/(^[\r\n\t]*)|([\r\n\t]*$)/gi, ''));
template = template.replace(matches[i], match.replace(/(^[\r\n?|\n]*)|([\r\n\t]*$)/gi, ''));
}
}
@@ -181,40 +182,43 @@ Please use the npm module instead - require('templates.js')
var regex = makeRegex(key), block;
if (!array[key].length) {
return template;
return template.replace(regex, '');
}
while (block = template.match(regex)) {
block = block[0].replace(makeBlockRegex(key), '');
var numblocks = array[key].length - 1,
iterator = 0,
result = '',
parsedBlock;
do {
parsedBlock = parse(block, array[key][iterator], bind, namespace, {iterator: iterator, total: numblocks}) + ((iterator < numblocks) ? '\r\n':'');
parsedBlock = parse(block, array[key][iterator], bind, namespace, {iterator: iterator, total: numblocks});
result += (!bind) ? parsedBlock : setBindContainer(parsedBlock, bind + namespace + iterator);
result = parseFunctions(block, result, {
data: array[key][iterator],
iterator: iterator,
numblocks: numblocks
});
result = checkConditional(result, '@first', iterator === 0);
result = checkConditional(result, '!@first', iterator !== 0);
result = checkConditional(result, '@last', iterator === numblocks);
result = checkConditional(result, '!@last', iterator !== numblocks);
result = result.replace(/^[\r\n?|\n|\t]*?|[\r\n?|\n|\t]*?$/g, '');
result = parseFunctions(block, result, {
data: array[key][iterator],
iterator: iterator,
numblocks: numblocks
});
if (bind) {
array[key][iterator].__template = block;
}
} while (iterator++ < numblocks);
template = template.replace(regex, result);
template = template.replace(regex, result.replace(/^[\r\n?|\n]|[\r\n?|\n]$/g, ''));
}
return template;
}
@@ -248,14 +252,14 @@ Please use the npm module instead - require('templates.js')
this['__' + key] = value;
var els = document.querySelectorAll('[data-binding="' + (this.__iterator !== false ? (bind + this.__namespace + this.__iterator) : bind) + '"]');
for (var el in els) {
if (els.hasOwnProperty(el)) {
if (this.__parent) {
var parent = this.__parent();
els[el].innerHTML = parse(parent.template, parent.data, false);
} else {
els[el].innerHTML = parse(this.__template, obj, false, this.__namespace);
els[el].innerHTML = parse(this.__template, obj, false, this.__namespace);
}
}
}
@@ -295,7 +299,7 @@ Please use the npm module instead - require('templates.js')
template = parse(template, obj[key], bind, namespace + key + '.');
} else {
template = parseValue(template, namespace + key, obj[key]);
if (bind && obj[key]) {
setupBindings({
obj: obj,

View File

@@ -93,7 +93,7 @@ var db = require('./database'),
category.pageCount = results.pageCount;
category.topic_row_size = 'col-md-9';
callback(null, category);
plugins.fireHook('filter:category.get', category, uid, callback);
});
});
};
@@ -237,10 +237,9 @@ var db = require('./database'),
};
Categories.markAsUnreadForAll = function(cid, callback) {
callback = callback || function() {};
db.delete('cid:' + cid + ':read_by_uid', function(err) {
if(typeof callback === 'function') {
callback(err);
}
callback(err);
});
};
@@ -295,6 +294,13 @@ var db = require('./database'),
db.getObjectField('category:' + cid, field, callback);
};
Categories.getMultipleCategoryFields = function(cids, fields, callback) {
var keys = cids.map(function(cid) {
return 'category:' + cid;
});
db.getObjectsFields(keys, fields, callback);
};
Categories.getCategoryFields = function(cid, fields, callback) {
db.getObjectFields('category:' + cid, fields, callback);
};

View File

@@ -365,6 +365,8 @@ accountsController.accountSettings = function(req, res, next) {
userData.settings = results.settings;
userData.languages = results.languages;
userData.disableEmailSubscriptions = meta.config.disableEmailSubscriptions !== undefined && parseInt(meta.config.disableEmailSubscriptions, 10) === 1;
res.render('account/settings', userData);
});
});
@@ -402,7 +404,11 @@ accountsController.uploadPicture = function (req, res, next) {
image.resizeImage(req.files.userPhoto.path, extension, 128, 128, next);
},
function(next) {
image.convertImageToPng(req.files.userPhoto.path, extension, next);
if (parseInt(meta.config['profile:convertProfileImageToPNG'], 10) === 1) {
image.convertImageToPng(req.files.userPhoto.path, extension, next);
} else {
next();
}
},
function(next) {
user.getUidByUserslug(req.params.userslug, next);
@@ -448,7 +454,7 @@ accountsController.uploadPicture = function (req, res, next) {
return plugins.fireHook('filter:uploadImage', req.files.userPhoto, done);
}
var convertToPNG = parseInt(meta.config['profile:convertProfileImageToPNG'], 10);
var convertToPNG = parseInt(meta.config['profile:convertProfileImageToPNG'], 10) === 1;
var filename = updateUid + '-profileimg' + (convertToPNG ? '.png' : extension);
user.getUserField(updateUid, 'uploadedpicture', function (err, oldpicture) {

View File

@@ -8,7 +8,8 @@ var categoriesController = {},
user = require('./../user'),
categories = require('./../categories'),
topics = require('./../topics'),
meta = require('./../meta');
meta = require('./../meta'),
plugins = require('./../plugins');
categoriesController.recent = function(req, res, next) {
var uid = req.user ? req.user.uid : 0;
@@ -19,7 +20,9 @@ categoriesController.recent = function(req, res, next) {
data['feeds:disableRSS'] = meta.config['feeds:disableRSS'] === '1' ? true : false;
res.render('recent', data);
plugins.fireHook('filter:category.get', data, uid, function(err, data) {
res.render('recent', data);
});
});
};
@@ -35,7 +38,9 @@ categoriesController.popular = function(req, res, next) {
data['feeds:disableRSS'] = meta.config['feeds:disableRSS'] === '1' ? true : false;
res.render('popular', {topics: data});
plugins.fireHook('filter:category.get', {topics: data}, uid, function(err, data) {
res.render('popular', data);
});
});
};
@@ -47,7 +52,9 @@ categoriesController.unread = function(req, res, next) {
return next(err);
}
res.render('unread', data);
plugins.fireHook('filter:category.get', data, uid, function(err, data) {
res.render('unread', data);
});
});
};
@@ -74,22 +81,16 @@ categoriesController.get = function(req, res, next) {
},
function(disabled, next) {
if (parseInt(disabled, 10) === 1) {
return next(new Error('category-disabled'));
return next(new Error('[[error:category-disabled]]'));
}
privileges.categories.get(cid, uid, function(err, categoryPrivileges) {
if (err) {
return next(err);
}
if (!categoryPrivileges.read) {
return next(new Error('[[error:no-privileges]]'));
}
next(null, categoryPrivileges);
});
privileges.categories.get(cid, uid, next);
},
function (privileges, next) {
if (!privileges.read) {
return next(new Error('[[error:no-privileges]]'));
}
user.getSettings(uid, function(err, settings) {
if (err) {
return next(err);
@@ -111,12 +112,6 @@ categoriesController.get = function(req, res, next) {
return next(err);
}
if (categoryData) {
if (parseInt(categoryData.disabled, 10) === 1) {
return next(new Error('[[error:category-disabled]]'));
}
}
categoryData.privileges = privileges;
next(err, categoryData);
});
@@ -189,7 +184,6 @@ categoriesController.get = function(req, res, next) {
active: x === parseInt(page, 10)
});
}
res.render('category', data);
});
};

View File

@@ -2,6 +2,7 @@
var tagsController = {},
async = require('async'),
nconf = require('nconf'),
topics = require('./../topics');
tagsController.getTag = function(req, res, next) {
@@ -13,7 +14,7 @@ tagsController.getTag = function(req, res, next) {
return next(err);
}
if (!tids || !tids.length) {
if (Array.isArray(tids) && !tids.length) {
topics.deleteTag(tag);
return res.render('tag', {topics: [], tag:tag});
}
@@ -22,6 +23,22 @@ tagsController.getTag = function(req, res, next) {
if (err) {
return next(err);
}
res.locals.metaTags = [
{
name: "title",
content: tag
},
{
property: 'og:title',
content: tag
},
{
property: "og:url",
content: nconf.get('url') + '/tags/' + tag
}
];
data.tag = tag;
res.render('tag', data);
});

View File

@@ -51,6 +51,10 @@ module.exports = function(db, module) {
});
};
module.getSetsMembers = function(keys, callback) {
throw new Error('not-implemented');
};
module.setCount = function(key, callback) {
module.getListRange(key, 0, -1, function(err, set) {
callback(err, set.length);

View File

@@ -91,6 +91,24 @@ module.exports = function(db, module) {
});
};
module.getSetsMembers = function(keys, callback) {
db.collection('objects').find({_key: {$in: keys}}, {_key: 1, members: 1}).toArray(function(err, data) {
if (err) {
return callback(err);
}
var sets = {};
data.forEach(function(set) {
sets[set._key] = set.members || [];
});
var returnData = new Array(keys.length);
for(var i=0; i<keys.length; ++i) {
returnData[i] = sets[keys[i]] || [];
}
callback(null, returnData);
});
};
module.setCount = function(key, callback) {
db.collection('objects').findOne({_key:key}, function(err, data) {
return callback(err, data ? data.members.length : 0);

View File

@@ -151,13 +151,15 @@ module.exports = function(db, module) {
if (err) {
return callback(err);
}
results = results.map(function(item) {
return item.value;
});
values = values.map(function(value) {
return results.indexOf(value) !== -1;
});
callback(err, results);
callback(null, values);
});
};

View File

@@ -7,6 +7,7 @@ module.exports = function(redisClient, module) {
};
module.setObjectField = function(key, field, value, callback) {
callback = callback || function() {};
redisClient.hset(key, field, value, callback);
};

View File

@@ -54,6 +54,14 @@ module.exports = function(redisClient, module) {
redisClient.smembers(key, callback);
};
module.getSetsMembers = function(keys, callback) {
var multi = redisClient.multi();
for (var i=0; i<keys.length; ++i) {
multi.smembers(keys[i]);
}
multi.exec(callback);
};
module.setCount = function(key, callback) {
redisClient.scard(key, callback);
};

View File

@@ -2,6 +2,7 @@
module.exports = function(redisClient, module) {
module.sortedSetAdd = function(key, score, value, callback) {
callback = callback || function() {};
redisClient.zadd(key, score, value, callback);
};

View File

@@ -247,9 +247,10 @@ var async = require('async'),
};
Favourites.getFavouritedUidsByPids = function(pids, callback) {
async.map(pids, function(pid, next) {
db.getSetMembers('pid:' + pid + ':users_favourited', next);
}, callback);
var sets = pids.map(function(pid) {
return 'pid:' + pid + ':users_favourited';
});
db.getSetsMembers(sets, callback);
};
}(exports));

View File

@@ -156,6 +156,10 @@
db.isSetMember('group:' + groupName + ':members', uid, callback);
};
Groups.isMembers = function(uids, groupName, callback) {
db.isSetMembers('group:' + groupName + ':members', uids, callback);
};
Groups.isMemberOfGroups = function(uid, groups, callback) {
groups = groups.map(function(groupName) {
return 'group:' + groupName + ':members';
@@ -434,7 +438,7 @@
});
};
Groups.getUserGroups = function(uid, callback) {
Groups.getUserGroups = function(uids, callback) {
var ignoredGroups = ['registered-users'];
db.getSetMembers('groups', function(err, groupNames) {
@@ -462,19 +466,22 @@
return 'group:' + group.name + ':members';
});
db.isMemberOfSets(groupSets, uid, function(err, isMembers) {
if (err) {
return callback(err);
}
for(var i=isMembers.length - 1; i>=0; --i) {
if (!isMembers[i]) {
groupData.splice(i, 1);
async.map(uids, function(uid, next) {
db.isMemberOfSets(groupSets, uid, function(err, isMembers) {
if (err) {
return next(err);
}
}
callback(null, groupData);
});
var memberOf = [];
isMembers.forEach(function(isMember, index) {
if (isMember) {
memberOf.push(groupData[index]);
}
});
next(null, memberOf);
});
}, callback);
});
});
};

View File

@@ -1,8 +1,7 @@
'use strict';
var fs = require('fs'),
gm = require('gm').subClass({imageMagick: true}),
meta = require('./meta');
gm = require('gm').subClass({imageMagick: true});
var image = {};
@@ -28,8 +27,7 @@ image.resizeImage = function(path, extension, width, height, callback) {
};
image.convertImageToPng = function(path, extension, callback) {
var convertToPNG = parseInt(meta.config['profile:convertProfileImageToPNG'], 10);
if(convertToPNG && extension !== '.png') {
if(extension !== '.png') {
gm(path).toBuffer('png', function(err, buffer) {
if (err) {
return callback(err);

View File

@@ -266,12 +266,19 @@ function setOnEmpty(key1, key2) {
function enableDefaultTheme(next) {
var meta = require('./meta');
winston.info('Enabling default theme: Lavender');
meta.themes.set({
type: 'local',
id: 'nodebb-theme-lavender'
}, next);
meta.configs.get('theme:id', function(err, id) {
if (err || id) {
winston.info('Previous theme detected, skipping enabling default theme');
return next(err);
}
winston.info('Enabling default theme: Lavender');
meta.themes.set({
type: 'local',
id: 'nodebb-theme-lavender'
}, next);
});
}
function createAdministrator(next) {

View File

@@ -6,6 +6,7 @@ var db = require('./database'),
user = require('./user'),
plugins = require('./plugins'),
meta = require('./meta'),
utils = require('../public/src/utils'),
notifications = require('./notifications'),
userNotifications = require('./user/notifications');
@@ -128,7 +129,7 @@ var db = require('./database'),
var self = parseInt(message.fromuid, 10) === parseInt(fromuid, 10);
message.fromUser = self ? userData[0] : userData[1];
message.toUser = self ? userData[1] : userData[0];
message.timestampISO = new Date(parseInt(message.timestamp, 10)).toISOString();
message.timestampISO = utils.toISOString(message.timestamp);
message.self = self ? 1 : 0;
message.newSet = false;
@@ -193,7 +194,11 @@ var db = require('./database'),
}
},
function(mids, next) {
db.getObjects(['message:' + mids[0], 'message:' + mids[1]], next);
if (typeof mids !== 'boolean') {
db.getObjects(['message:' + mids[0], 'message:' + mids[1]], next);
} else {
next(null, mids);
}
},
function(messages, next) {
if (typeof messages !== 'boolean') {
@@ -216,32 +221,27 @@ var db = require('./database'),
return callback(err);
}
async.parallel({
unreadUids: async.apply(db.isSortedSetMembers, 'uid:' + uid + ':chats:unread', uids),
users: async.apply(user.getMultipleUserFields, uids, ['username', 'picture', 'uid'])
}, function(err, results) {
db.isSortedSetMembers('uid:' + uid + ':chats:unread', uids, function(err, unreadUids) {
if (err) {
return callback(err);
}
var users = results.users;
for (var i=0; i<users.length; ++i) {
users[i].unread = results.unreadUids[i];
}
user.isOnline(uids, function(err, users) {
if (err) {
return callback(err);
}
users = users.filter(function(user, index) {
return !!user.uid;
});
async.map(users, function(userData, next) {
user.isOnline(userData.uid, function(err, data) {
if (err) {
return next(err);
users.forEach(function(user, index) {
if (user) {
user.unread = unreadUids[index];
}
userData.status = data.status;
next(null, userData);
});
}, callback);
users = users.filter(function(user) {
return !!user.uid;
});
callback(null, users);
});
});
});
};
@@ -258,7 +258,9 @@ var db = require('./database'),
db.sortedSetAdd('uid:' + uid + ':chats:unread', Date.now(), toUid, callback);
};
// todo #1798 -- this utility method creates a room name given an array of uids.
/*
todo #1798 -- this utility method creates a room name given an array of uids.
Messaging.uidsToRoom = function(uids, callback) {
uid = parseInt(uid, 10);
if (typeof uid === 'number' && Array.isArray(roomUids)) {
@@ -274,7 +276,7 @@ var db = require('./database'),
} else {
callback(new Error('invalid-uid-or-participant-uids'));
}
};
};*/
Messaging.verifySpammer = function(uid, callback) {
var messagesToCompare = 10;

View File

@@ -39,6 +39,15 @@ module.exports = function(Meta) {
return next();
} else {
var configObj = JSON.parse(file.toString());
// Minor adjustments for API output
configObj.type = 'local';
if (configObj.screenshot) {
configObj.screenshot_url = nconf.get('relative_path') + '/css/previews/' + configObj.id
} else {
configObj.screenshot_url = nconf.get('relative_path') + '/images/themes/default.png';
}
next(err, configObj);
}
});

View File

@@ -9,6 +9,7 @@ module.exports = function(Meta) {
var tests = {
isCategory: /^category\/\d+\/?/,
isTopic: /^topic\/\d+\/?/,
isTag: /^tags\/[\s\S]+\/?/,
isUserPage: /^user\/[^\/]+(\/[\w]+)?/
};
@@ -42,6 +43,12 @@ module.exports = function(Meta) {
var tid = urlFragment.match(/topic\/(\d+)/)[1];
require('../topics').getTopicField(tid, 'title', callback);
} else if (tests.isTag.test(urlFragment)) {
var tag = urlFragment.match(/tags\/([\s\S]+)/)[1];
translator.translate('[[pages:tags, ' + tag + ']]', language, function(translated) {
callback(null, translated);
});
} else if (tests.isUserPage.test(urlFragment)) {
var matches = urlFragment.match(/user\/([^\/]+)\/?([\w]+)?/),
userslug = matches[1],

View File

@@ -16,7 +16,7 @@ var app,
middleware.isAdmin = function(req, res, next) {
if (!req.user) {
return res.redirect('/login?next=admin');
return res.redirect(nconf.get('relative_path') + '/login?next=admin');
}
user.isAdministrator((req.user && req.user.uid) ? req.user.uid : 0, function (err, isAdmin) {

View File

@@ -41,7 +41,7 @@ function routeThemeScreenshots(app, themes) {
(function(id, path) {
fs.exists(path, function(exists) {
if (exists) {
app.get('/css/previews/' + id, function(req, res) {
app.get(relativePath + '/css/previews/' + id, function(req, res) {
res.sendfile(path);
});
}
@@ -173,7 +173,7 @@ module.exports = function(app, data) {
if(meta.config.cookieDomain) {
cookie.domain = meta.config.cookieDomain;
}
app.use(session({
store: db.sessionStore,
secret: nconf.get('secret'),

View File

@@ -6,7 +6,6 @@ var app,
path = require('path'),
winston = require('winston'),
validator = require('validator'),
fs = require('fs'),
nconf = require('nconf'),
plugins = require('./../plugins'),
meta = require('./../meta'),
@@ -209,6 +208,7 @@ middleware.renderHeader = function(req, res, callback) {
var uid = req.user ? parseInt(req.user.uid, 10) : 0;
var custom_header = {
uid: uid,
'navigation': []
};

View File

@@ -285,12 +285,22 @@ var async = require('async'),
return winston.error(err.message);
}
async.filter(nids, function(nid, next) {
db.getObjectField('notifications:' + nid, 'datetime', function(err, datetime) {
next(!err && parseInt(datetime, 10) < cutoffTime);
var keys = nids.map(function(nid) {
return 'notifications:' + nid;
});
db.getObjectsFields(keys, ['nid', 'datetime'], function(err, notifs) {
if (err) {
return winston.error(err.message);
}
var expiredNids = notifs.filter(function(notif) {
return notif && parseInt(notif.datetime, 10) < cutoffTime;
}).map(function(notif) {
return notif.nid;
});
}, function(expiredNids) {
async.each(expiredNids, function(nid, next) {
async.eachLimit(expiredNids, 50, function(nid, next) {
async.parallel([
function(next) {
db.setRemove('notifications', nid, next);
@@ -303,15 +313,15 @@ var async = require('async'),
next(err);
});
}, function(err) {
if (!err) {
if (process.env.NODE_ENV === 'development') {
winston.info('[notifications.prune] Notification pruning completed. ' + numPruned + ' expired notification' + (numPruned !== 1 ? 's' : '') + ' removed.');
}
var diff = process.hrtime(start);
events.log('Pruning notifications took : ' + (diff[0] * 1e3 + diff[1] / 1e6) + ' ms');
} else {
winston.error('Encountered error pruning notifications: ' + err.message);
if (err) {
return winston.error('Encountered error pruning notifications: ' + err.message);
}
if (process.env.NODE_ENV === 'development') {
winston.info('[notifications.prune] Notification pruning completed. ' + numPruned + ' expired notification' + (numPruned !== 1 ? 's' : '') + ' removed.');
}
var diff = process.hrtime(start);
events.log('Pruning '+ numPruned + ' notifications took : ' + (diff[0] * 1e3 + diff[1] / 1e6) + ' ms');
});
});
});

View File

@@ -93,7 +93,7 @@ var async = require('async'),
};
Posts.getPostsByTid = function(tid, set, start, end, reverse, callback) {
db[reverse ? 'getSortedSetRevRange' : 'getSortedSetRange'](set, start, end, function(err, pids) {
Posts.getPidsFromSet(set, start, end, reverse, function(err, pids) {
if(err) {
return callback(err);
}
@@ -102,31 +102,15 @@ var async = require('async'),
return callback(null, []);
}
Posts.getPostsByPids(pids, function(err, posts) {
if(err) {
return callback(err);
}
if(!Array.isArray(posts) || !posts.length) {
return callback(null, []);
}
plugins.fireHook('filter:post.getPosts', {tid: tid, posts: posts}, function(err, data) {
if(err) {
return callback(err);
}
if(!data || !Array.isArray(data.posts)) {
return callback(null, []);
}
callback(null, data.posts);
});
});
Posts.getPostsByPids(pids, tid, callback);
});
};
Posts.getPostsByPids = function(pids, callback) {
Posts.getPidsFromSet = function(set, start, end, reverse, callback) {
db[reverse ? 'getSortedSetRevRange' : 'getSortedSetRange'](set, start, end, callback);
};
Posts.getPostsByPids = function(pids, tid, callback) {
var keys = [];
for(var x=0, numPids=pids.length; x<numPids; ++x) {
@@ -155,7 +139,23 @@ var async = require('async'),
next(null, postData);
});
}, callback);
}, function(err, posts) {
if (err) {
return callback(err);
}
plugins.fireHook('filter:post.getPosts', {tid: tid, posts: posts}, function(err, data) {
if (err) {
return callback(err);
}
if (!data || !Array.isArray(data.posts)) {
return callback(null, []);
}
callback(null, data.posts);
});
});
});
};
@@ -222,21 +222,31 @@ var async = require('async'),
};
Posts.getUserInfoForPosts = function(uids, callback) {
user.getMultipleUserFields(uids, ['uid', 'username', 'userslug', 'reputation', 'postcount', 'picture', 'signature', 'banned'], function(err, userData) {
async.parallel({
groups: function(next) {
groups.getUserGroups(uids, next);
},
userData: function(next) {
user.getMultipleUserFields(uids, ['uid', 'username', 'userslug', 'reputation', 'postcount', 'picture', 'signature', 'banned'], next);
}
}, function(err, results) {
if (err) {
return callback(err);
}
var userData = results.userData;
for(var i=0; i<userData.length; ++i) {
userData[i].groups = results.groups[i];
}
async.map(userData, function(userData, next) {
var userInfo = {
uid: userData.uid || 0,
username: userData.username || '[[global:guest]]',
userslug: userData.userslug || '',
reputation: userData.reputation || 0,
postcount: userData.postcount || 0,
banned: parseInt(userData.banned, 10) === 1,
picture: userData.picture || user.createGravatarURLFromEmail('')
};
userData.uid = userData.uid || 0;
userData.username = userData.username || '[[global:guest]]';
userData.userslug = userData.userslug || '';
userData.reputation = userData.reputation || 0;
userData.postcount = userData.postcount || 0;
userData.banned = parseInt(userData.banned, 10) === 1;
userData.picture = userData.picture || user.createGravatarURLFromEmail('');
async.parallel({
signature: function(next) {
@@ -247,18 +257,14 @@ var async = require('async'),
},
customProfileInfo: function(next) {
plugins.fireHook('filter:posts.custom_profile_info', {profile: [], uid: userData.uid}, next);
},
groups: function(next) {
groups.getUserGroups(userData.uid, next);
}
}, function(err, results) {
if (err) {
return next(err);
}
userInfo.signature = results.signature;
userInfo.custom_profile_info = results.customProfileInfo.profile;
userInfo.groups = results.groups;
next(null, userInfo);
userData.signature = results.signature;
userData.custom_profile_info = results.customProfileInfo.profile;
next(null, userData);
});
}, callback);
});

View File

@@ -104,6 +104,28 @@ module.exports = function(privileges) {
});
};
privileges.categories.isAdminOrMod = function(cids, uid, callback) {
async.parallel({
isModerators: function(next) {
user.isModerator(uid, cids, next);
},
isAdmin: function(next) {
user.isAdministrator(uid, next);
}
}, function(err, results) {
if (err) {
return callback(err);
}
var returnData = new Array(cids.length);
for (var i=0; i<cids.length; ++i) {
returnData[i] = results.isAdmin || results.isModerators[i];
}
callback(null, returnData);
});
};
privileges.categories.canMoveAllTopics = function(currentCid, targetCid, uid, callback) {
async.parallel({
isAdministrator: function(next) {

View File

@@ -135,10 +135,10 @@ function getModerators(req, res, next) {
});
}
var templatesListingCache = [];
var templatesListingCache = {};
function getTemplatesListing(req, res, next) {
if (templatesListingCache.length) {
if (templatesListingCache.availableTemplates && templatesListingCache.templatesConfig) {
return res.json(templatesListingCache);
}
@@ -148,11 +148,18 @@ function getTemplatesListing(req, res, next) {
},
extended: function(next) {
plugins.fireHook('filter:templates.get_virtual', [], next);
}
},
config: function(next) {
fs.readFile(path.join(nconf.get('views_dir'), 'config.json'), function(err, config) {
config = JSON.parse(config.toString());
plugins.fireHook('filter:templates.get_config', config, next);
});
},
}, function(err, results) {
if (err) {
return next(err);
}
var data = [];
data = results.views.filter(function(value, index, self) {
return self.indexOf(value) === index;
@@ -161,8 +168,13 @@ function getTemplatesListing(req, res, next) {
});
data = data.concat(results.extended);
templatesListingCache = data;
res.json(data);
templatesListingCache = {
availableTemplates: data,
templatesConfig: results.config
};
res.json(templatesListingCache);
});
}

View File

@@ -97,9 +97,6 @@
return res.redirect(nconf.get('relative_path') + '/register' + (err.message ? '?error=' + err.message : ''));
}
delete userData['password-confirm'];
delete userData['_csrf'];
user.create(userData, function(err, uid) {
if (err || !uid) {
return res.redirect(nconf.get('relative_path') + '/register');

View File

@@ -19,7 +19,7 @@ var nconf = require('nconf'),
function mainRoutes(app, middleware, controllers) {
app.get('/', middleware.buildHeader, controllers.home);
app.get('/api/home', controllers.home);
app.get('/api', controllers.home);
app.get('/login', middleware.redirectToAccountIfLoggedIn, middleware.buildHeader, controllers.login);
app.get('/api/login', middleware.redirectToAccountIfLoggedIn, controllers.login);
@@ -160,7 +160,7 @@ module.exports = function(app, middleware) {
app.render.apply(app, arguments);
};
app.all(relativePath + '/api/*', middleware.updateLastOnlineTime, middleware.prepareAPI);
app.all(relativePath + '/api/?*', middleware.updateLastOnlineTime, middleware.prepareAPI);
app.all(relativePath + '/api/admin/*', middleware.admin.isAdmin, middleware.prepareAPI);
app.all(relativePath + '/admin/*', middleware.admin.isAdmin);
app.get(relativePath + '/admin', middleware.admin.isAdmin);
@@ -247,4 +247,4 @@ function catch404(req, res, next) {
} else {
res.type('txt').send('Not found');
}
}
}

View File

@@ -76,10 +76,8 @@ var path = require('path'),
},
render: function(callback) {
if (sitemap.obj !== undefined && sitemap.obj.cache.length) {
console.log('using sitemap from cache!');
sitemap.obj.toXML(callback);
} else {
console.log('generating new sitemap!', sitemap.obj);
async.parallel([sitemap.getStaticUrls, sitemap.getDynamicUrls], function(err, urls) {
urls = urls[0].concat(urls[1]);
sitemap.obj = sm.createSitemap({

View File

@@ -4,8 +4,8 @@ var async = require('async'),
db = require('../database'),
categories = require('../categories'),
privileges = require('../privileges'),
meta = require('../meta'),
user = require('../user'),
websockets = require('./index'),
SocketCategories = {};
@@ -44,6 +44,10 @@ SocketCategories.loadMore = function(socket, data, callback) {
return callback(err);
}
if (!results.privileges.read) {
return callback(new Error('[[error:no-privileges]]'));
}
var start = parseInt(data.after, 10),
end = start + results.settings.topicsPerPage - 1;
@@ -70,4 +74,9 @@ SocketCategories.lastTopicIndex = function(socket, cid, callback) {
db.sortedSetCard('categories:' + cid + ':tid', callback);
};
SocketCategories.getUsersInCategory = function(socket, cid, callback) {
var uids = websockets.getUidsInRoom('category_' + cid);
user.getMultipleUserFields(uids, ['uid', 'userslug', 'username', 'picture'], callback);
};
module.exports = SocketCategories;

View File

@@ -287,17 +287,6 @@ function updateRoomBrowsingText(roomName) {
return;
}
function getUidsInRoom() {
var uids = [];
var clients = io.sockets.clients(roomName);
for(var i=0; i<clients.length; ++i) {
if (uids.indexOf(clients[i].uid) === -1 && clients[i].uid !== 0) {
uids.push(clients[i].uid);
}
}
return uids;
}
function getAnonymousCount() {
var clients = io.sockets.clients(roomName);
var anonCount = 0;
@@ -310,7 +299,7 @@ function updateRoomBrowsingText(roomName) {
return anonCount;
}
var uids = getUidsInRoom(),
var uids = Sockets.getUidsInRoom(roomName),
anonymousCount = getAnonymousCount();
user.getMultipleUserFields(uids, ['uid', 'username', 'userslug', 'picture', 'status'], function(err, users) {
@@ -328,6 +317,18 @@ function updateRoomBrowsingText(roomName) {
});
}
Sockets.getUidsInRoom = function(roomName) {
var uids = [];
var clients = io.sockets.clients(roomName);
for(var i=0; i<clients.length; ++i) {
if (uids.indexOf(clients[i].uid) === -1 && clients[i].uid !== 0) {
uids.push(clients[i].uid);
}
}
return uids;
};
Sockets.emitTopicPostStats = emitTopicPostStats;
function emitTopicPostStats(callback) {
db.getObjectFields('global', ['topicCount', 'postCount'], function(err, data) {

View File

@@ -161,6 +161,8 @@ SocketPosts.edit = function(socket, data, callback) {
return callback(new Error('[[error:invalid-data]]'));
} else if (!data.title || data.title.length < parseInt(meta.config.minimumTitleLength, 10)) {
return callback(new Error('[[error:title-too-short, ' + meta.config.minimumTitleLength + ']]'));
} else if (data.title.length > parseInt(meta.config.maximumTitleLength, 10)) {
return callback(new Error('[[error:title-too-long, ' + meta.config.maximumTitleLength + ']]'));
} else if (!data.content || data.content.length < parseInt(meta.config.minimumPostLength, 10)) {
return callback(new Error('[[error:content-too-short, ' + meta.config.minimumPostLength + ']]'));
}
@@ -241,37 +243,18 @@ SocketPosts.getPrivileges = function(socket, pid, callback) {
};
SocketPosts.getFavouritedUsers = function(socket, pid, callback) {
favourites.getFavouritedUidsByPids([pid], function(err, data) {
if(err) {
return callback(err);
}
if(!Array.isArray(data) || !data.length) {
callback(null, "");
callback(null, []);
}
var max = 5; //hardcoded
var finalText = "";
var pid_uids = data[0];
var rest_amount = 0;
if (pid_uids.length > max) {
rest_amount = pid_uids.length - max;
pid_uids = pid_uids.slice(0, max);
}
user.getUsernamesByUids(pid_uids, function(err, usernames) {
if(err) {
return callback(err);
}
finalText = usernames.join(', ') + (rest_amount > 0 ?
(" and " + rest_amount + (rest_amount > 1 ? " others" : " other")) : "");
callback(null, finalText);
});
user.getUsernamesByUids(pid_uids, callback);
});
};

View File

@@ -54,6 +54,15 @@ SocketTopics.post = function(socket, data, callback) {
});
};
SocketTopics.enter = function(socket, tid, callback) {
if (!tid || !socket.uid) {
return;
}
SocketTopics.markAsRead(socket, tid);
topics.markTopicNotificationsRead(tid, socket.uid);
topics.increaseViewCount(tid);
};
SocketTopics.postcount = function(socket, tid, callback) {
topics.getTopicField(tid, 'postcount', callback);
};
@@ -141,25 +150,58 @@ SocketTopics.markCategoryTopicsRead = function(socket, cid, callback) {
};
SocketTopics.markAsUnreadForAll = function(socket, tids, callback) {
if(!Array.isArray(tids)) {
if (!Array.isArray(tids)) {
return callback(new Error('[[error:invalid-tid]]'));
}
async.each(tids, function(tid, next) {
topics.markAsUnreadForAll(tid, function(err) {
if(err) {
return next(err);
}
if (!socket.uid) {
return callback(new Error('[[error:no-privileges]]'));
}
db.sortedSetAdd('topics:recent', Date.now(), tid, function(err) {
if(err) {
user.isAdministrator(socket.uid, function(err, isAdmin) {
if (err) {
return callback(err);
}
async.each(tids, function(tid, next) {
async.waterfall([
function(next) {
threadTools.exists(tid, next);
},
function(exists, next) {
if (!exists) {
return next(new Error('[[error:invalid-tid]]'));
}
topics.getTopicField(tid, 'cid', next);
},
function(cid, next) {
user.isModerator(socket.uid, cid, next);
}
], function(err, isMod) {
if (err) {
return next(err);
}
topics.pushUnreadCount();
next();
if (!isAdmin && !isMod) {
return next(new Error('[[error:no-privileges]]'));
}
topics.markAsUnreadForAll(tid, function(err) {
if(err) {
return next(err);
}
db.sortedSetAdd('topics:recent', Date.now(), tid, function(err) {
if(err) {
return next(err);
}
topics.pushUnreadCount();
next();
});
});
});
});
}, callback);
}, callback);
});
};
SocketTopics.delete = function(socket, data, callback) {
@@ -333,20 +375,31 @@ SocketTopics.loadMore = function(socket, data, callback) {
return callback(new Error('[[error:invalid-data]]'));
}
user.getSettings(socket.uid, function(err, settings) {
if(err) {
async.parallel({
settings: function(next) {
user.getSettings(socket.uid, next);
},
privileges: function(next) {
privileges.topics.get(data.tid, socket.uid, next);
}
}, function(err, results) {
if (err) {
return callback(err);
}
if (!results.privileges.read) {
return callback(new Error('[[error:no-privileges]]'));
}
var start = Math.max(parseInt(data.after, 10) - 1, 0),
end = start + settings.postsPerPage - 1;
end = start + results.settings.postsPerPage - 1;
var set = 'tid:' + data.tid + ':posts',
reverse = false;
if (settings.topicPostSort === 'newest_to_oldest') {
if (results.settings.topicPostSort === 'newest_to_oldest') {
reverse = true;
} else if (settings.topicPostSort === 'most_votes') {
} else if (results.settings.topicPostSort === 'most_votes') {
reverse = true;
set = 'tid:' + data.tid + ':posts:votes';
}
@@ -356,7 +409,7 @@ SocketTopics.loadMore = function(socket, data, callback) {
topics.getTopicPosts(data.tid, set, start, end, socket.uid, reverse, next);
},
privileges: function(next) {
privileges.topics.get(data.tid, socket.uid, next);
next(null, results.privileges);
},
'reputation:disabled': function(next) {
next(null, parseInt(meta.config['reputation:disabled'], 10) === 1);

View File

@@ -5,6 +5,7 @@ var async = require('async'),
groups = require('../groups'),
topics = require('../topics'),
messaging = require('../messaging'),
plugins = require('../plugins'),
utils = require('./../../public/src/utils'),
meta = require('../meta'),
SocketUser = {};
@@ -79,7 +80,9 @@ SocketUser.reset.commit = function(socket, data, callback) {
};
SocketUser.isOnline = function(socket, uid, callback) {
user.isOnline(uid, callback);
user.isOnline([uid], function(err, data) {
callback(err, Array.isArray(data) ? data[0] : null);
});
};
SocketUser.changePassword = function(socket, data, callback) {
@@ -159,16 +162,30 @@ SocketUser.changePicture = function(socket, data, callback) {
SocketUser.follow = function(socket, data, callback) {
if (socket.uid && data) {
user.follow(socket.uid, data.uid, callback);
toggleFollow('follow', socket.uid, data.uid, callback);
}
};
SocketUser.unfollow = function(socket, data, callback) {
if (socket.uid && data) {
user.unfollow(socket.uid, data.uid, callback);
toggleFollow('unfollow', socket.uid, data.uid, callback);
}
};
function toggleFollow(method, uid, theiruid, callback) {
user[method](uid, theiruid, function(err) {
if (err) {
return callback(err);
}
plugins.fireHook('action:user.' + method, {
fromUid: uid,
toUid: theiruid
});
callback();
});
}
SocketUser.getSettings = function(socket, data, callback) {
if (socket.uid) {
if (socket.uid === parseInt(data.uid, 10)) {
@@ -217,24 +234,23 @@ SocketUser.setTopicSort = function(socket, sort, callback) {
}
};
SocketUser.getOnlineUsers = function(socket, data, callback) {
SocketUser.getOnlineUsers = function(socket, uids, callback) {
var returnData = {};
if(!data) {
if (!uids) {
return callback(new Error('[[error:invalid-data]]'));
}
function getUserStatus(uid, next) {
SocketUser.isOnline(socket, uid, function(err, data) {
if(err) {
return next(err);
}
returnData[uid] = data;
next();
});
}
user.isOnline(uids, function(err, userData) {
if (err) {
return callback(err);
}
async.each(data, getUserStatus, function(err) {
callback(err, returnData);
userData.forEach(function(user) {
if (user) {
returnData[user.uid] = user;
}
});
callback(null, returnData);
});
};

View File

@@ -120,8 +120,7 @@ var winston = require('winston'),
plugins.fireHook('action:topic.lock', {
tid: tid,
isLocked: lock,
uid: uid,
timestamp: Date.now()
uid: uid
});
emitTo('topic_' + tid);
@@ -165,8 +164,7 @@ var winston = require('winston'),
plugins.fireHook('action:topic.pin', {
tid: tid,
isPinned: pin,
uid: uid,
timestamp: Date.now()
uid: uid
});
emitTo('topic_' + tid);
@@ -214,8 +212,7 @@ var winston = require('winston'),
tid: tid,
fromCid: oldCid,
toCid: cid,
uid: uid,
timestamp: Date.now()
uid: uid
});
});
};

View File

@@ -1,12 +1,11 @@
"use strict";
var async = require('async'),
gravatar = require('gravatar'),
validator = require('validator'),
db = require('./database'),
posts = require('./posts'),
utils = require('./../public/src/utils'),
utils = require('../public/src/utils'),
plugins = require('./plugins'),
user = require('./user'),
categories = require('./categories'),
@@ -26,11 +25,7 @@ var async = require('async'),
Topics.getTopicData = function(tid, callback) {
Topics.getTopicsData([tid], function(err, topics) {
if (err) {
return callback(err);
}
callback(null, topics ? topics[0] : null);
callback(err, Array.isArray(topics) && topics.length ? topics[0] : null);
});
};
@@ -128,7 +123,7 @@ var async = require('async'),
nextStart: 0
};
if (!tids || !tids.length) {
if (!Array.isArray(tids) || !tids.length) {
return callback(null, returnTopics);
}
@@ -170,96 +165,83 @@ var async = require('async'),
};
Topics.getTopicsByTids = function(tids, uid, callback) {
if (!Array.isArray(tids) || tids.length === 0) {
if (!Array.isArray(tids) || !tids.length) {
return callback(null, []);
}
var categoryCache = {},
privilegeCache = {},
userCache = {};
function loadTopicInfo(topicData, next) {
if (!topicData) {
return next(null, null);
}
function isTopicVisible(topicData, topicInfo) {
if (parseInt(topicInfo.categoryData.disabled, 10) === 1) {
return false;
}
var deleted = parseInt(topicData.deleted, 10) !== 0;
return !deleted || (deleted && topicInfo.privileges.view_deleted) || parseInt(topicData.uid, 10) === parseInt(uid, 10);
}
async.parallel({
hasread: function(next) {
Topics.hasReadTopic(topicData.tid, uid, next);
},
teaser: function(next) {
Topics.getTeaser(topicData.tid, next);
},
privileges: function(next) {
if (privilegeCache[topicData.cid]) {
return next(null, privilegeCache[topicData.cid]);
}
privileges.categories.get(topicData.cid, uid, next);
},
categoryData: function(next) {
if (categoryCache[topicData.cid]) {
return next(null, categoryCache[topicData.cid]);
}
categories.getCategoryFields(topicData.cid, ['name', 'slug', 'icon', 'bgColor', 'color', 'disabled'], next);
},
user: function(next) {
if (userCache[topicData.uid]) {
return next(null, userCache[topicData.uid]);
}
user.getUserFields(topicData.uid, ['username', 'userslug', 'picture'], next);
},
tags: function(next) {
Topics.getTopicTagsObjects(topicData.tid, next);
}
}, function(err, topicInfo) {
if(err) {
return next(err);
}
privilegeCache[topicData.cid] = topicInfo.privileges;
categoryCache[topicData.cid] = topicInfo.categoryData;
userCache[topicData.uid] = topicInfo.user;
if (!isTopicVisible(topicData, topicInfo)) {
return next(null, null);
}
topicData.pinned = parseInt(topicData.pinned, 10) === 1;
topicData.locked = parseInt(topicData.locked, 10) === 1;
topicData.deleted = parseInt(topicData.deleted, 10) === 1;
topicData.unread = !(topicInfo.hasread && parseInt(uid, 10) !== 0);
topicData.unreplied = parseInt(topicData.postcount, 10) <= 1;
topicData.category = topicInfo.categoryData;
topicData.teaser = topicInfo.teaser;
topicData.user = topicInfo.user;
topicData.tags = topicInfo.tags;
next(null, topicData);
});
}
Topics.getTopicsData(tids, function(err, topics) {
function mapFilter(array, field) {
return array.map(function(topic) {
return topic[field];
}).filter(function(value, index, array) {
return array.indexOf(value) === index;
});
}
if (err) {
return callback(err);
}
async.mapSeries(topics, loadTopicInfo, function(err, topics) {
if(err) {
var uids = mapFilter(topics, 'uid');
var cids = mapFilter(topics, 'cid');
async.parallel({
users: function(next) {
user.getMultipleUserFields(uids, ['uid', 'username', 'userslug', 'picture'], next);
},
categories: function(next) {
categories.getMultipleCategoryFields(cids, ['cid', 'name', 'slug', 'icon', 'bgColor', 'color', 'disabled'], next);
},
hasRead: function(next) {
Topics.hasReadTopics(tids, uid, next);
},
isAdminOrMod: function(next) {
privileges.categories.isAdminOrMod(cids, uid, next);
},
teasers: function(next) {
Topics.getTeasers(tids, next);
},
tags: function(next) {
Topics.getTopicsTagsObjects(tids, next);
}
}, function(err, results) {
function arrayToObject(array, field) {
var obj = {};
for (var i=0; i<array.length; ++i) {
obj[array[i][field]] = array[i];
}
return obj;
}
if (err) {
return callback(err);
}
var users = arrayToObject(results.users, 'uid');
var categories = arrayToObject(results.categories, 'cid');
var isAdminOrMod = {};
cids.forEach(function(cid, index) {
isAdminOrMod[cid] = results.isAdminOrMod[index];
});
for (var i=0; i<topics.length; ++i) {
topics[i].category = categories[topics[i].cid];
topics[i].category.disabled = parseInt(topics[i].category.disabled, 10) === 1;
topics[i].user = users[topics[i].uid];
topics[i].teaser = results.teasers[i];
topics[i].tags = results.tags[i];
topics[i].pinned = parseInt(topics[i].pinned, 10) === 1;
topics[i].locked = parseInt(topics[i].locked, 10) === 1;
topics[i].deleted = parseInt(topics[i].deleted, 10) === 1;
topics[i].unread = !(results.hasRead[i] && parseInt(uid, 10) !== 0);
topics[i].unreplied = parseInt(topics[i].postcount, 10) <= 1;
}
topics = topics.filter(function(topic) {
return !!topic;
return !topic.category.disabled &&
(!topic.deleted || (topic.deleted && isAdminOrMod[topic.cid]) ||
parseInt(topic.uid, 10) === parseInt(uid, 10));
});
plugins.fireHook('filter:topics.get', topics, callback);
@@ -274,12 +256,29 @@ var async = require('async'),
}
async.parallel({
mainPost: function(next) {
Topics.getMainPost(tid, uid, next);
},
posts: function(next) {
Topics.getTopicPosts(tid, set, start, end, uid, reverse, next);
},
posts: function(next) {
posts.getPidsFromSet(set, start, end, reverse, function(err, pids) {
if (err) {
return next(err);
}
pids = topicData.mainPid ? [topicData.mainPid].concat(pids) : pids;
if (!pids.length) {
return next(null, []);
}
posts.getPostsByPids(pids, tid, function(err, posts) {
if (err) {
return next(err);
}
start = parseInt(start, 10);
for(var i=0; i<posts.length; ++i) {
posts[i].index = start + i;
}
posts[0].index = 0;
Topics.addPostData(posts, uid, next);
});
});
},
category: function(next) {
Topics.getCategoryData(tid, next);
},
@@ -298,7 +297,7 @@ var async = require('async'),
}
topicData.category = results.category;
topicData.posts = results.mainPost.concat(results.posts);
topicData.posts = results.posts;
topicData.tags = results.tags;
topicData.thread_tools = results.threadTools;
topicData.pageCount = results.pageCount;
@@ -307,9 +306,7 @@ var async = require('async'),
topicData.locked = parseInt(topicData.locked, 10) === 1;
topicData.pinned = parseInt(topicData.pinned, 10) === 1;
plugins.fireHook('filter:topic.get', topicData, function(err, topicData) {
callback(null, topicData);
});
plugins.fireHook('filter:topic.get', topicData, callback);
});
});
};
@@ -322,7 +319,7 @@ var async = require('async'),
if (!parseInt(mainPid, 10)) {
return callback(null, []);
}
posts.getPostsByPids([mainPid], function(err, postData) {
posts.getPostsByPids([mainPid], tid, function(err, postData) {
if (err) {
return callback(err);
}
@@ -336,12 +333,71 @@ var async = require('async'),
};
Topics.getTeasers = function(tids, callback) {
if(!Array.isArray(tids)) {
return callback(null, []);
}
async.map(tids, Topics.getTeaser, callback);
async.map(tids, function(tid, next) {
db.getSortedSetRevRange('tid:' + tid + ':posts', 0, 0, function(err, data) {
next(err, Array.isArray(data) && data.length ? data[0] : null);
});
}, function(err, pids) {
if (err) {
return callback(err);
}
var postKeys = pids.map(function(pid) {
return 'post:' + pid;
});
async.parallel({
indices: function(next) {
var sets = tids.map(function(tid) {
return 'tid:' + tid + ':posts';
});
db.sortedSetsRanks(sets, pids, next);
},
posts: function(next) {
db.getObjectsFields(postKeys, ['pid', 'uid', 'timestamp'], next);
}
}, function(err, results) {
if (err) {
return callback(err);
}
var indices = results.indices.map(function(index) {
if (!utils.isNumber(index)) {
return 1;
}
return parseInt(index, 10) + 2;
});
var uids = results.posts.map(function(post) {
return post.uid;
}).filter(function(uid, index, array) {
return array.indexOf(uid) === index;
});
user.getMultipleUserFields(uids, ['uid', 'username', 'userslug', 'picture'], function(err, userData) {
if (err) {
return callback(err);
}
var users = {};
userData.forEach(function(user) {
users[user.uid] = user;
});
results.posts.forEach(function(post, index) {
post.user = users[post.uid];
post.index = indices[index];
post.timestamp = utils.toISOString(post.timestamp);
});
callback(err, results.posts);
});
});
});
};
Topics.getTeaser = function(tid, callback) {
@@ -413,8 +469,12 @@ var async = require('async'),
};
Topics.isOwner = function(tid, uid, callback) {
uid = parseInt(uid, 10);
if (uid === 0) {
return callback(null, false);
}
Topics.getTopicField(tid, 'uid', function(err, author) {
callback(err, parseInt(author, 10) === parseInt(uid, 10));
callback(err, parseInt(author, 10) === uid);
});
};

View File

@@ -47,7 +47,6 @@ module.exports = function(Topics) {
return '';
}
tag = tag.trim().toLowerCase();
tag = tag.replace(/&/g, '&amp;');
tag = tag.replace(/[,\/#!$%\^\*;:{}=_`<>'"~()?\|]/g, '');
tag = tag.substr(0, meta.config.maximumTagLength || 15);
var matches = tag.match(/^[.-]*(.+?)[.-]*$/);
@@ -93,6 +92,25 @@ module.exports = function(Topics) {
});
};
Topics.getTopicsTagsObjects = function(tids, callback) {
var sets = tids.map(function(tid) {
return 'topic:' + tid + ':tags';
});
db.getSetsMembers(sets, function(err, members) {
if (err) {
return callback(err);
}
members.forEach(function(tags, index) {
if (Array.isArray(tags)) {
members[index] = mapToObject(tags);
}
})
callback(null, members);
});
};
function mapToObject(tags) {
if (!tags) {
return tags;

View File

@@ -23,26 +23,26 @@ module.exports = function(Topics) {
done = false;
uid = parseInt(uid, 10);
if(uid === 0) {
if (uid === 0) {
return callback(null, unreadTids);
}
async.whilst(function() {
return unreadTids.length < 21 && !done;
}, function(callback) {
}, function(next) {
Topics.getLatestTids(start, stop, 'month', function(err, tids) {
if (err) {
return callback(err);
return next(err);
}
if (tids && !tids.length) {
done = true;
return callback();
return next();
}
Topics.hasReadTopics(tids, uid, function(err, read) {
if(err) {
return callback(err);
if (err) {
return next(err);
}
var newtids = tids.filter(function(tid, index) {
@@ -50,16 +50,15 @@ module.exports = function(Topics) {
});
privileges.topics.filter('read', newtids, uid, function(err, newtids) {
if(err) {
return callback(err);
if (err) {
return next(err);
}
unreadTids.push.apply(unreadTids, newtids);
start = stop + 1;
stop = start + 19;
callback();
next();
});
});
});

View File

@@ -68,8 +68,9 @@ var bcrypt = require('bcryptjs'),
if (err) {
return callback(err);
}
callback(null, modifyUserData(users, fieldsToRemove));
plugins.fireHook('filter:user.removeFields', fieldsToRemove, function(err, fields) {
callback(err, modifyUserData(users, fields));
});
});
};
@@ -94,7 +95,9 @@ var bcrypt = require('bcryptjs'),
return callback(err);
}
callback(null, modifyUserData(users, []));
plugins.fireHook('filter:user.removeFields', [], function(err, fields) {
callback(err, modifyUserData(users, fields));
});
});
};
@@ -239,36 +242,32 @@ var bcrypt = require('bcryptjs'),
};
User.getUsers = function(uids, callback) {
function loadUserInfo(user, callback) {
if (!user) {
return callback(null, user);
async.parallel({
userData: function(next) {
User.getMultipleUserFields(uids, ['uid', 'username', 'userslug', 'picture', 'status', 'banned', 'postcount', 'reputation'], next);
},
isAdmin: function(next) {
User.isAdministrator(uids, next);
},
isOnline: function(next) {
db.isSortedSetMembers('users:online', uids, next);
}
async.waterfall([
function(next) {
User.isAdministrator(user.uid, next);
},
function(isAdmin, next) {
user.status = !user.status ? 'online' : user.status;
user.administrator = isAdmin;
user.banned = parseInt(user.banned, 10) === 1;
db.isSortedSetMember('users:online', user.uid, next);
},
function(isMember, next) {
if (!isMember) {
user.status = 'offline';
}
next(null, user);
}
], callback);
}
User.getMultipleUserFields(uids, ['uid', 'username', 'userslug', 'picture', 'status', 'banned', 'postcount', 'reputation'], function(err, usersData) {
}, function(err, results) {
if (err) {
return callback(err);
}
async.map(usersData, loadUserInfo, callback);
results.userData.forEach(function(user, index) {
if (!user) {
return;
}
user.status = !user.status ? 'online' : user.status;
user.status = !results.isOnline[index] ? 'offline' : user.status;
user.administrator = results.isAdmin[index];
user.banned = parseInt(user.banned, 10) === 1;
});
callback(err, results.userData);
});
};
@@ -410,29 +409,40 @@ var bcrypt = require('bcryptjs'),
};
User.isAdministrator = function(uid, callback) {
groups.isMember(uid, 'administrators', callback);
if (Array.isArray(uid)) {
groups.isMembers(uid, 'administrators', callback);
} else {
groups.isMember(uid, 'administrators', callback);
}
};
User.isOnline = function(uid, callback) {
User.getUserFields(uid, ['username', 'userslug', 'picture', 'status', 'reputation', 'postcount'] , function(err, data) {
if(err) {
User.isOnline = function(uids, callback) {
if (!Array.isArray(uids)) {
uids = [uids];
}
User.getMultipleUserFields(uids, ['uid', 'username', 'userslug', 'picture', 'status', 'reputation', 'postcount'] , function(err, userData) {
if (err) {
return callback(err);
}
var websockets = require('./socket.io');
var online = websockets.isUserOnline(uid);
data.status = online ? (data.status || 'online') : 'offline';
userData = userData.map(function(user) {
var online = websockets.isUserOnline(user.uid);
user.status = online ? (user.status || 'online') : 'offline';
if(data.status === 'offline') {
online = false;
}
if (user.status === 'offline') {
online = false;
}
data.online = online;
data.uid = uid;
data.timestamp = Date.now();
data.rooms = websockets.getUserRooms(uid);
user.online = online;
user.timestamp = Date.now();
user.rooms = websockets.getUserRooms(user.uid);
return user;
});
callback(null, data);
callback(null, userData);
});
};

View File

@@ -12,19 +12,38 @@ var async = require('async'),
module.exports = function(User) {
User.create = function(userData, callback) {
userData = userData || {};
User.create = function(data, callback) {
var gravatar = User.createGravatarURLFromEmail(data.email);
var timestamp = Date.now();
var password = data.password;
var userData = {
'username': data.username.trim(),
'email': data.email,
'joindate': timestamp,
'picture': gravatar,
'gravatarpicture': gravatar,
'fullname': '',
'location': '',
'birthday': '',
'website': '',
'signature': '',
'uploadedpicture': '',
'profileviews': 0,
'reputation': 0,
'postcount': 0,
'lastposttime': 0,
'banned': 0,
'status': 'online'
};
userData.userslug = utils.slugify(userData.username);
userData.username = userData.username.trim();
if (userData.email !== undefined) {
userData.email = userData.email.trim();
userData.email = validator.escape(userData.email);
}
var password = userData.password;
userData.password = null;
async.parallel({
emailValid: function(next) {
if (userData.email) {
@@ -84,7 +103,7 @@ module.exports = function(User) {
}
},
customFields: function(next) {
plugins.fireHook('filter:user.custom_fields', userData, next);
plugins.fireHook('filter:user.custom_fields', [], next);
},
userData: function(next) {
plugins.fireHook('filter:user.create', userData, next);
@@ -94,7 +113,14 @@ module.exports = function(User) {
return callback(err);
}
userData = utils.merge(results.userData, results.customFields);
var customData = {};
results.customFields.forEach(function(customField) {
if (data[customField]) {
customData[customField] = data[customField];
}
});
userData = utils.merge(results.userData, customData);
var userNameChanged = !!results.renamedUsername;
@@ -104,37 +130,14 @@ module.exports = function(User) {
}
db.incrObjectField('global', 'nextUid', function(err, uid) {
if(err) {
if (err) {
return callback(err);
}
var gravatar = User.createGravatarURLFromEmail(userData.email);
var timestamp = Date.now();
userData = utils.merge({
'uid': uid,
'username': userData.username,
'userslug': userData.userslug,
'fullname': '',
'location': '',
'birthday': '',
'website': '',
'email': userData.email || '',
'signature': '',
'joindate': timestamp,
'picture': gravatar,
'gravatarpicture': gravatar,
'uploadedpicture': '',
'profileviews': 0,
'reputation': 0,
'postcount': 0,
'lastposttime': 0,
'banned': 0,
'status': 'online'
}, userData);
userData.uid = uid;
db.setObject('user:' + uid, userData, function(err) {
if(err) {
if (err) {
return callback(err);
}

View File

@@ -18,14 +18,11 @@ module.exports = function(User) {
},
function(next) {
deleteTopics(uid, next);
},
function(next) {
deleteAccount(uid, next);
}
], function(err) {
if (err) {
return callback(err);
}
deleteAccount(uid, callback);
});
], callback);
};
function deletePosts(uid, callback) {

View File

@@ -22,6 +22,11 @@ module.exports = function(User) {
};
User.sendDailyDigests = function() {
var digestsDisabled = meta.config.disableEmailSubscriptions !== undefined && parseInt(meta.config.disableEmailSubscriptions, 10) === 1;
if (digestsDisabled) {
return winston.log('[user/jobs] Did not send daily digests because subscription system is disabled.');
}
async.parallel({
recent: function(next) {
topics.getLatestTopics(0, 0, 10, 'day', next);

View File

@@ -55,6 +55,7 @@ var async = require('async'),
}
db.sortedSetRemove(set, nidsToUniqueIds[nid]);
db.deleteObjectField('uid:' + uid + ':notifications:uniqueId:nid', nidsToUniqueIds[nid]);
return next();
}
@@ -141,7 +142,7 @@ var async = require('async'),
}
notifs = notifs.filter(function(notif) {
return notif !== null;
return !!notif;
}).sort(function(a, b) {
return parseInt(b.datetime, 10) - parseInt(a.datetime, 10);
}).map(function(notif) {