Merge remote-tracking branch 'refs/remotes/origin/master' into develop

v1.18.x
Barış Soner Uşaklı 7 years ago
commit 155e20d216

1
.gitignore vendored

@ -29,6 +29,7 @@ pidfile
/public/sounds
/public/uploads
/test/uploads
# compiled files
/public/stylesheet.css

@ -2,7 +2,7 @@
"name": "nodebb",
"license": "GPL-3.0",
"description": "NodeBB Forum",
"version": "1.7.3",
"version": "1.7.4",
"homepage": "http://www.nodebb.org",
"repository": {
"type": "git",
@ -64,16 +64,13 @@
"nodebb-plugin-dbsearch": "2.0.9",
"nodebb-plugin-emoji": "2.1.0",
"nodebb-plugin-emoji-android": "2.0.0",
"nodebb-plugin-markdown": "8.2.2",
"nodebb-plugin-markdown": "8.3.0",
"nodebb-plugin-mentions": "2.2.2",
"nodebb-plugin-soundpack-default": "1.0.0",
"nodebb-plugin-spam-be-gone": "0.5.1",
"nodebb-rewards-essentials": "0.0.11",
"nodebb-theme-lavender": "5.0.1",
"nodebb-theme-persona": "7.2.18",
"nodebb-theme-slick": "1.1.4",
"nodebb-theme-vanilla": "8.1.8",
"nodebb-theme-persona": "7.2.16",
"nodebb-theme-persona": "7.2.19",
"nodebb-theme-slick": "1.1.4",
"nodebb-theme-vanilla": "8.1.7",
"nodebb-widget-essentials": "4.0.1",
@ -107,17 +104,17 @@
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"coveralls": "^3.0.0",
"eslint": "^4.14.0",
"eslint-config-airbnb-base": "^12.1.0",
"eslint-plugin-import": "^2.8.0",
"grunt": "^1.0.1",
"grunt-contrib-watch": "^1.0.0",
"jsdom": "^11.5.1",
"mocha": "^4.1.0",
"mocha-lcov-reporter": "^1.3.0",
"nyc": "^11.4.1",
"smtp-server": "^3.4.1"
"coveralls": "^3.0.0",
"eslint": "^4.14.0",
"eslint-config-airbnb-base": "^12.1.0",
"eslint-plugin-import": "^2.8.0",
"grunt": "^1.0.1",
"grunt-contrib-watch": "^1.0.0",
"jsdom": "^11.5.1",
"mocha": "^4.1.0",
"mocha-lcov-reporter": "^1.3.0",
"nyc": "^11.4.1",
"smtp-server": "^3.4.1"
},
"bugs": {
"url": "https://github.com/NodeBB/NodeBB/issues"

@ -76,6 +76,12 @@ function setupRoutes() {
app.get('/', welcome);
app.post('/', install);
app.post('/launch', launch);
app.get('/ping', ping);
app.get('/sping', ping);
}
function ping(req, res) {
res.status(200).send(req.path === '/sping' ? 'healthy' : '200');
}
function welcome(req, res) {

@ -0,0 +1,7 @@
{
"upload-file": "Upload File",
"filename": "Filename",
"size/filecount": "Size / Filecount",
"confirm-delete": "Do you really want to delete this file?",
"filecount": "%1 files"
}

@ -17,6 +17,7 @@
"manage/post-queue": "Post Queue",
"manage/groups": "Groups",
"manage/ip-blacklist": "IP Blacklist",
"manage/uploads": "Uploads",
"section-settings": "Settings",
"settings/general": "General",

@ -38,6 +38,7 @@
"flag_title": "Flag this post for moderation",
"merged_message": "This topic has been merged into <a href=\"/topic/%1\">%2</a>",
"deleted_message": "This topic has been deleted. Only users with topic management privileges can see it.",
"following_topic.message": "You will now be receiving notifications when somebody posts to this topic.",

@ -1,7 +1,7 @@
{
"x-b": "%1 octets",
"x-mb": "%1 Mo",
"x-gb": "%1 Go",
"x-b": "%1 b",
"x-mb": "%1 Mb",
"x-gb": "%1 Gb",
"uptime-seconds": "Disponibilité en secondes",
"uptime-days": "Disponibilité en jours",
@ -29,7 +29,7 @@
"redis.memory-frag-ratio": "Ratio de fragmentation de la mémoire",
"redis.total-connections-recieved": "Connexions totales reçues",
"redis.total-commands-processed": "Commandes totales exécutées",
"redis.iops": "Opérations par seconde",
"redis.iops": "Opérations instantanées par seconde",
"redis.keyspace-hits": "Keyspace Hits",
"redis.keyspace-misses": "Keyspace Misses",
"redis.raw-info": "Informations brutes Redis"

@ -1,7 +1,7 @@
{
"custom-css": "Custom CSS/LESS",
"custom-css.description": "Enter your own CSS/LESS declarations here, which will be applied after all other styles.",
"custom-css.enable": "Enable Custom CSS/LESS",
"custom-css": "CSS/LESS personnalisé",
"custom-css.description": "Entrez vos propres déclarations CSS/LESS ici, qui seront appliquées après tous les autres styles.",
"custom-css.enable": "Activer le CSS/LESS personnalisé",
"custom-js": "Javascript personnalisé",
"custom-js.description": "Entrez votre Javascript ici. Celui-ci sera exécute après le chargement complet de la page.",

@ -5,8 +5,8 @@
"create-modify": "Créer et modifier les mots-clés",
"description": "Sélectionnez les mot-clés par clic ou glisser-déposer, maintenez shift pour en sélectionner plusieurs.",
"create": "Créer le mot-clés",
"modify": "Modifier le mot-clés",
"rename": "Rename Tags",
"modify": "Modifier les mots-clés",
"rename": "Renommer les mots-clés",
"delete": "Supprimer les mots-clés sélectionnés",
"search": "Chercher des mots-clés...",
"settings": "Cliquez <a href=\"%1\">ici</a> pour visiter la page de paramètres des mots clés.",

@ -76,7 +76,7 @@
"alerts.remove-admin-success": "L'utilisateur n'est plus administrateur",
"alerts.make-global-mod-success": "L'utilisateur est maintenant modérateur global",
"alerts.confirm-remove-global-mod": "Voulez-vous vraiment supprimer ce modérateur global?",
"alerts.remove-global-mod-success": "User is no longer global moderator.",
"alerts.remove-global-mod-success": "L'utilisateur n'est plus un modérateur global.",
"alerts.make-moderator-success": "L'utilisateur est maintenant modérateur",
"alerts.confirm-remove-moderator": "Voulez-vous vraiment supprimer ce modérateur?",
"alerts.remove-moderator-success": "L'utilisateur n'est plus modérateur",

@ -6,7 +6,7 @@
"thresholds": "Seuils d'activité",
"min-rep-downvote": "Réputation minimum pour les votes négatifs",
"min-rep-flag": "Réputation minimum pour signaler un message",
"min-rep-website": "Minimum reputation to add \"Website\" to user profile",
"min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile",
"min-rep-signature": "Minimum reputation to add \"Signature\" to user profile"
"min-rep-website": "Réputation minimum pour ajouter \"Site internet\" au profil utilisateur",
"min-rep-aboutme": "Réputation minimum pour ajouter \"À propos\" au profil utilisateur",
"min-rep-signature": "Réputation minimum pour ajouter \"Signature\" au profil utilisateur"
}

@ -8,7 +8,7 @@
"invalid-cid": "ID de catégorie invalide",
"invalid-tid": "ID de sujet invalide",
"invalid-pid": "ID de message invalide",
"invalid-uid": "ID utilisateur invalide",
"invalid-uid": "ID d'utilisateur invalide",
"invalid-username": "Nom d'utilisateur invalide",
"invalid-email": "Email invalide",
"invalid-title": "Titre invalide",
@ -18,23 +18,23 @@
"invalid-username-or-password": "Veuillez entrer un nom d'utilisateur et un mot de passe",
"invalid-search-term": "Données de recherche invalides",
"invalid-url": "URL invalide",
"csrf-invalid": "Nous ne pouvons pas vous connectez, possiblement car votre session a expiré. Merci de réessayer.",
"csrf-invalid": "Nous ne pouvons pas vous connectez, probablement car votre session a expiré. Merci de réessayer.",
"invalid-pagination-value": "Valeur de pagination invalide. Celle-ci doit être comprise entre %1 et %2.",
"username-taken": "Nom dutilisateur déjà utilisé",
"email-taken": "Email déjà utilisé",
"email-not-confirmed": "Votre adresse email n'est pas confirmée, cliquez ici pour la valider.",
"email-not-confirmed-chat": "Il ne vous est pas possible d'utiliser le chat tant que votre adresse email n'a pas été vérifiée. Veuillez cliquer ici pour confirmer votre adresse email.",
"email-not-confirmed-email-sent": "Votre adresse email n'a pas encore été confirmée. Merci de vérifier l'email de confirmation dans votre boîte de reception.",
"email-not-confirmed-email-sent": "Votre adresse email n'a pas encore été confirmée. Merci de vérifier l'email de confirmation dans votre boîte de réception.",
"no-email-to-confirm": "Ce forum requiert une vérification de votre adresse email. Veuillez cliquer ici pour entrer une adresse.",
"email-confirm-failed": "Votre adresse email n'a pas pu être vérifiée. Veuillez ré-essayer plus tard.",
"confirm-email-already-sent": "L'email de confirmation a déjà été envoyé. Veuillez attendre %1 minute(s) avant de redemander un nouvel envoi.",
"sendmail-not-found": "L'application d'envoi de mail est introuvable, assurez-vous qu'elle est installée et que l'utilisateur servant à démarrer NodeBB ait des droits suffisants.",
"sendmail-not-found": "L'application d'envoi de mail est introuvable, assurez-vous qu'elle est installée et que l'utilisateur ayant démarré NodeBB ait des droits suffisants.",
"username-too-short": "Nom d'utilisateur trop court",
"username-too-long": "Nom d'utilisateur trop long",
"password-too-long": "Mot de passe trop long",
"user-banned": "Utilisateur banni",
"user-banned-reason": "Désolé, ce compte a été banni (Raison : %1)",
"user-banned-reason-until": "Désolé, ce compte a été banni jusque %1 (Raison : %2).",
"user-banned-reason-until": "Désolé, ce compte a été banni jusqu'au %1 (Raison : %2).",
"user-too-new": "Désolé, vous devez attendre encore %1 seconde(s) avant d'envoyer votre premier message",
"blacklisted-ip": "Désolé, votre adresse IP a été bannie de cette communauté. Si vous pensez que c'est une erreur, veuillez contacter un administrateur.",
"ban-expiry-missing": "Veuillez entrer une date de fin de banissement.",
@ -63,24 +63,24 @@
"post-delete-duration-expired-days-hours": "Vous ne pouvez supprimer un message que pendant %1 jour(s) et %2 heure(s) après l'avoir posté.",
"cant-delete-topic-has-reply": "Vous ne pouvez pas supprimer votre sujet s'il a au moins une réponse.",
"cant-delete-topic-has-replies": "Vous ne pouvez pas supprimer votre sujet s'il a au moins %1 réponses.",
"content-too-short": "Veuillez entrer un message plus long. %1 caractère(s) minimum.",
"content-too-long": "Veuillez poster un message plus cours. Les messages ne peuvent être plus long que %1 caractère(s).",
"title-too-short": "Veuillez entrer un titre plus long. %1 caractère(s) minimum.",
"content-too-short": "Veuillez entrer un message plus long. Les messages doivent contenir au moins %1 caractère(s).",
"content-too-long": "Veuillez poster un message plus court. Les messages ne peuvent être plus long que %1 caractère(s).",
"title-too-short": "Veuillez entrer un titre plus long. Les titres doivent contenir au moins %1 caractère(s).",
"title-too-long": "Veuillez entrer un titre plus court. Les titres ne peuvent excéder %1 caractère(s).",
"category-not-selected": "Aucune catégorie sélectionnée",
"too-many-posts": "Vous ne pouvez poster que toutes les %1 seconde(s).",
"too-many-posts": "Vous ne pouvez poster que toutes les %1 seconde(s) - merci de patienter avant de publier à nouveau.",
"too-many-posts-newbie": "En tant que nouvel utilisateur, vous ne pouvez poster que toutes les %1 seconde(s) jusqu'à ce que vous obteniez une réputation de %2 - patientez avant de publier de nouveau.",
"tag-too-short": "Veuillez entrer un mot-clé plus long. Les mots-clés doivent contenir au moins %1 caractère(s).",
"tag-too-long": "Veuillez entrer un mot-clé plus court. Les mot-clés ne peuvent faire plus de %1 caractère(s).",
"tag-too-long": "Veuillez entrer un mot-clé plus court. Les mot-clés ne peuvent excéder %1 caractère(s).",
"not-enough-tags": "Pas assez de mots-clés. Les sujets doivent avoir au moins %1 mots-clé(s).",
"too-many-tags": "Trop de mots-clés. Les sujets ne peuvent avoir au plus que %1 mots-clé(s).",
"still-uploading": "Veuillez patienter pendant le téléchargement.",
"file-too-big": "La taille maximale autorisée pour un fichier est de %1 kb. Veuillez envoyer un fichier plus petit.",
"guest-upload-disabled": "Le téléversement est désactivé pour les Invités.",
"still-uploading": "Veuillez patienter pendant l'envoi des fichiers.",
"file-too-big": "La taille maximale autorisée pour un fichier est de %1 ko. Veuillez envoyer un fichier plus petit.",
"guest-upload-disabled": "L'envoi de fichiers a été désactivé pour les invités",
"already-bookmarked": "Vous avez déjà mis un marque-page",
"already-unbookmarked": "Vous avez déjà retiré un marque-page",
"cant-ban-other-admins": "Vous ne pouvez pas bannir les autres administrateurs !",
"cant-remove-last-admin": "Vous seul êtes administrateur. Ajouter un autre utilisateur en tant qu'administrateur avant de vous en retirer.",
"cant-remove-last-admin": "Vous êtes le seul administrateur. Ajoutez un autre utilisateur en tant qu'administrateur avant de vous retirer.",
"cant-delete-admin": "Veuillez retirer les droits d'administration de ce compte avant de tenter de le supprimer.",
"invalid-image": "Image invalide",
"invalid-image-type": "Type d'image invalide. Les types autorisés sont: %1",
@ -105,41 +105,41 @@
"uploads-are-disabled": "Les envois sont désactivés",
"signature-too-long": "La signature ne peut dépasser %1 caractère(s).",
"about-me-too-long": "Votre texte \"à propos de moi\" ne peut dépasser %1 caractère(s).",
"cant-chat-with-yourself": "Vous ne pouvez chatter avec vous même !",
"chat-restricted": "Cet utilisateur a restreint les ses messages de chat. Il doit d'abord s'abonner à votre compte avant que vous puissiez discuter avec lui.",
"cant-chat-with-yourself": "Vous ne pouvez discuter avec vous-même !",
"chat-restricted": "Cet utilisateur a restreint ses messages de chat. Il doit d'abord s'abonner à votre compte avant que vous puissiez discuter avec lui.",
"chat-disabled": "Système de chat désactivé",
"too-many-messages": "Vous avez envoyé trop de messages, veuillez patienter un instant.",
"invalid-chat-message": "Message de Chat invalide",
"invalid-chat-message": "Message de chat invalide",
"chat-message-too-long": "Les messages de discussion ne peuvent pas être plus longs que %1 caractères.",
"cant-edit-chat-message": "Vous n'avez pas l'autorisation de modifier ce message",
"cant-remove-last-user": "Vous ne pouvez pas supprimer le dernier utilisateur",
"cant-delete-chat-message": "Vous n'avez pas l'autorisation de supprimer ce message",
"chat-edit-duration-expired": "Vous n'êtes autorisé à modifier des messages qu'après %1 seconde(s) de les avoir postés",
"chat-delete-duration-expired": "Vous n'êtes autorisé à supprimer des messages qu'après %1 seconde(s) de les avoir postés",
"chat-edit-duration-expired": "Vous n'êtes autorisé à modifier des messages que pendant %1 seconde(s) après les avoir postés",
"chat-delete-duration-expired": "Vous n'êtes autorisé à supprimer des messages que pendant %1 seconde(s) après les avoir postés",
"already-voting-for-this-post": "Vous avez déjà voté pour ce message.",
"reputation-system-disabled": "Le système de réputation est désactivé",
"downvoting-disabled": "Les votes négatifs ne sont pas autorisés",
"not-enough-reputation-to-downvote": "Vous n'avez pas une réputation assez élevée pour noter négativement ce message",
"not-enough-reputation-to-flag": "Vous n'avez pas une réputation assez élevée pour signaler ce message",
"not-enough-reputation-min-rep-website": "You do not have enough reputation to add a website",
"not-enough-reputation-min-rep-aboutme": "You do not have enough reputation to add an about me",
"not-enough-reputation-min-rep-signature": "You do not have enough reputation to add a signature",
"not-enough-reputation-min-rep-website": "Vous n'avez pas une réputation assez élevée pour ajouter un site internet",
"not-enough-reputation-min-rep-aboutme": "Vous n'avez pas une réputation assez élevée pour ajouter un à propos",
"not-enough-reputation-min-rep-signature": "Vous n'avez pas une réputation assez élevée pour ajouter une signature",
"already-flagged": "Vous avez déjà signalé ce message",
"self-vote": "Vous ne pouvez pas voter sur votre propre message",
"reload-failed": "NodeBB a rencontré un problème lors du rechargement : \"% 1\" . NodeBB continuera de fonctionner côté client, même si vous devez annuler ce que vous avez fait juste avant de recharger .",
"reload-failed": "NodeBB a rencontré un problème lors du rechargement : \"%1\" . NodeBB continuera de fonctionner côté client, même si vous devriez annuler ce que vous avez fait juste avant de recharger.",
"registration-error": "Erreur d'enregistrement",
"parse-error": "Une erreur est survenue en analysant la réponse du serveur",
"wrong-login-type-email": "Veuillez utiliser votre adresse email pour vous connecter",
"wrong-login-type-username": "Veuillez utiliser votre identifiant pour vous connecter",
"sso-registration-disabled": "L'enregistrement a été désactivé pour les comptes %1, s'il vous plaît, enregistrez vous avec un adresse mail en premier",
"invite-maximum-met": "Vous avez invité la quantité maximale de personnes (%1 de %2).",
"no-session-found": "Pas de session de connexion trouvé!",
"sso-registration-disabled": "L'enregistrement a été désactivé pour les comptes %1, merci de vous enregistrer avec une adresse mail avant",
"invite-maximum-met": "Vous avez invité la quantité maximale de personnes (%1 sur %2).",
"no-session-found": "Pas de session de connexion trouvée !",
"not-in-room": "L'utilisateur n'est pas dans cette salle",
"no-users-in-room": "Aucun utilisateur dans cette salle",
"cant-kick-self": "Vous ne pouvez pas vous exclure vous-même du groupe",
"no-users-selected": "Aucun utilisateur sélectionné",
"invalid-home-page-route": "Route de page d'accueil invalide",
"invalid-session": "Session Interrompue",
"invalid-home-page-route": "Chemin vers la page d'accueil invalide",
"invalid-session": "Session interrompue",
"invalid-session-text": "Il semble que votre session ne soit plus active, ou que le serveur ne la reconnaisse plus. Merci de rafraichir cette page.",
"no-topics-selected": "Aucun sujet sélectionné !"
}

@ -20,7 +20,7 @@
"users/search": "Rechercher des utilisateurs",
"notifications": "Notifications",
"tags": "Mots-clés",
"tag": "Topics tagged under &quot;%1&quot;",
"tag": "Sujets marqués comme \"%1\"",
"register": "Créer un compte",
"registration-complete": "Inscription terminée",
"login": "Connectez-vous à votre compte",

@ -121,9 +121,9 @@
"downvoting-disabled": "Negatief stemmen is uitgeschakeld",
"not-enough-reputation-to-downvote": "Je hebt onvoldoende reputatie om een negatieve stem uit te mogen brengen",
"not-enough-reputation-to-flag": "Je hebt onvoldoende reputatie om dit bericht aan de beheerders te mogen melden",
"not-enough-reputation-min-rep-website": "You do not have enough reputation to add a website",
"not-enough-reputation-min-rep-aboutme": "You do not have enough reputation to add an about me",
"not-enough-reputation-min-rep-signature": "You do not have enough reputation to add a signature",
"not-enough-reputation-min-rep-website": "Je hebt onvoldoende reputatie om een website toe te mogen voegen",
"not-enough-reputation-min-rep-aboutme": "Je hebt onvoldoende reputatie om een \"Over mij\" toe te mogen voegen",
"not-enough-reputation-min-rep-signature": "Je hebt onvoldoende reputatie om een handtekening toe te mogen voegen",
"already-flagged": "Je hebt deze post al gerapporteerd",
"self-vote": "Het is niet mogelijk om op je eigen bericht te stemmen",
"reload-failed": "Tijdens het herladen van \"%1\" is NodeBB een fout of probleem tegengekomen. NodeBB blijft operationeel. Echter het is verstandig om de oorzaak te onderzoeken en wellicht de vorige actie, voor het herladen, ongedaan te maken.",

@ -9,7 +9,7 @@
"updated": "Bijgewerkt",
"target-purged": "The content this flag referred to has been purged and is no longer available.",
"quick-filters": "Quick Filters",
"quick-filters": "Snelfilters",
"filter-active": "Er zijn een of meer filters actief in deze lijst van markeringen",
"filter-reset": "Filters verwijderen",
"filters": "Filter opties",
@ -19,13 +19,13 @@
"filter-type-all": "Alle inhoud",
"filter-type-post": "Bericht",
"filter-state": "Status",
"filter-assignee": "Assignee UID",
"filter-assignee": "UID van toewijzer",
"filter-cid": "Categorie",
"filter-quick-mine": "Toegewezen aan mij",
"filter-cid-all": "Alle categorieën",
"apply-filters": "Filters toepassen",
"quick-links": "Quick Links",
"quick-links": "Snelkoppelingen",
"flagged-user": "Flagged User",
"view-profile": "Profiel bekijken",
"start-new-chat": "Begin een nieuwe chat",

@ -20,7 +20,7 @@
"users/search": "Zoek Gebruiker",
"notifications": "Notificaties",
"tags": "Tags",
"tag": "Topics tagged under &quot;%1&quot;",
"tag": "Onderwerpen getagd onder &quot;%1&quot;",
"register": "Registeer een gebruikersaccount",
"registration-complete": "Registratie compleet",
"login": "Login met u gebruikersaccount in",

@ -0,0 +1,35 @@
'use strict';
define('admin/manage/uploads', ['uploader'], function (uploader) {
var Uploads = {};
Uploads.init = function () {
$('#upload').on('click', function () {
uploader.show({
title: '[[admin/manage/uploads:upload-file]]',
route: config.relative_path + '/api/admin/upload/file',
params: { folder: ajaxify.data.currentFolder },
}, function () {
ajaxify.refresh();
});
});
$('.delete').on('click', function () {
var file = $(this).parents('[data-path]');
bootbox.confirm('[[admin/manage/uploads:confirm-delete]]', function (ok) {
if (!ok) {
return;
}
socket.emit('admin.uploads.delete', file.attr('data-path'), function (err) {
if (err) {
return app.alertError(err.message);
}
file.remove();
});
});
});
};
return Uploads;
});

@ -1,7 +1,7 @@
'use strict';
define('forum/categories', ['components', 'translator', 'benchpress'], function (components, translator, Benchpress) {
define('forum/categories', ['components'], function (components) {
var categories = {};
$(window).on('action:ajaxify.start', function (ev, data) {
@ -36,7 +36,8 @@ define('forum/categories', ['components', 'translator', 'benchpress'], function
var recentPosts = category.find('[component="category/posts"]');
parseAndTranslate([post], function (html) {
app.parseAndTranslate('partials/categories/lastpost', 'posts', { posts: [post] }, function (html) {
html.find('.post-content img:not(.not-responsive)').addClass('img-responsive');
html.hide();
if (recentPosts.length === 0) {
html.appendTo(category);
@ -57,16 +58,5 @@ define('forum/categories', ['components', 'translator', 'benchpress'], function
});
}
function parseAndTranslate(posts, callback) {
Benchpress.parse('partials/categories/lastpost', 'posts', { posts: posts }, function (html) {
translator.translate(html, function (translatedHTML) {
translatedHTML = $(translatedHTML);
translatedHTML.find('.post-content img:not(.not-responsive)').addClass('img-responsive');
callback(translatedHTML);
});
});
}
return categories;
});

@ -550,7 +550,7 @@
value = value ? value.split(' ') : [];
['noopener', 'noreferrer'].forEach(function (property) {
if (!value.includes(property)) {
if (value.indexOf(property) === -1) {
value.push(property);
}
});

@ -106,7 +106,6 @@ helpers.getUserDataByUserSlug = function (userslug, callerUID, callback) {
userData.moderationNote = undefined;
}
userData.uid = userData.uid;
userData.yourid = callerUID;
userData.theirid = userData.uid;
userData.isTargetAdmin = results.isTargetAdmin;

@ -4,16 +4,109 @@ var path = require('path');
var async = require('async');
var nconf = require('nconf');
var mime = require('mime');
var fs = require('fs');
var meta = require('../../meta');
var file = require('../../file');
var image = require('../../image');
var plugins = require('../../plugins');
var pagination = require('../../pagination');
var allowedImageTypes = ['image/png', 'image/jpeg', 'image/pjpeg', 'image/jpg', 'image/gif', 'image/svg+xml'];
var uploadsController = module.exports;
uploadsController.get = function (req, res, next) {
var currentFolder = path.join(nconf.get('upload_path'), req.query.dir || '');
if (!currentFolder.startsWith(nconf.get('upload_path'))) {
return next(new Error('[[error:invalid-path]]'));
}
var itemsPerPage = 20;
var itemCount = 0;
var page = parseInt(req.query.page, 10) || 1;
async.waterfall([
function (next) {
fs.readdir(currentFolder, next);
},
function (files, next) {
files = files.filter(function (filename) {
return filename !== '.gitignore';
});
itemCount = files.length;
var start = Math.max(0, (page - 1) * itemsPerPage);
var stop = start + itemsPerPage;
files = files.slice(start, stop);
filesToData(currentFolder, files, next);
},
function (files) {
files.sort(function (a, b) {
if (a.isDirectory && !b.isDirectory) {
return -1;
} else if (!a.isDirectory && b.isDirectory) {
return 1;
}
return 0;
});
res.render('admin/manage/uploads', {
currentFolder: currentFolder.replace(nconf.get('upload_path'), ''),
files: files,
breadcrumbs: buildBreadcrumbs(currentFolder),
pagination: pagination.create(page, Math.ceil(itemCount / itemsPerPage), req.query),
});
},
], next);
};
function buildBreadcrumbs(currentFolder) {
var crumbs = [];
var parts = currentFolder.replace(nconf.get('upload_path'), '').split(path.sep);
var currentPath = '';
parts.forEach(function (part) {
var dir = path.join(currentPath, part);
crumbs.push({
text: part || 'Uploads',
url: part ? '/admin/manage/uploads?dir=' + dir : '/admin/manage/uploads',
});
currentPath = dir;
});
return crumbs;
}
function filesToData(currentDir, files, callback) {
async.map(files, function (file, next) {
var stat;
async.waterfall([
function (next) {
fs.stat(path.join(currentDir, file), next);
},
function (_stat, next) {
stat = _stat;
if (stat.isDirectory()) {
fs.readdir(path.join(currentDir, file), next);
} else {
next(null, []);
}
},
function (filesInDir, next) {
var url = nconf.get('upload_url') + currentDir.replace(nconf.get('upload_path'), '') + '/' + file;
next(null, {
name: file,
path: path.join(currentDir, file).replace(nconf.get('upload_path'), ''),
url: url,
fileCount: filesInDir.length - 1, // ignore .gitignore
size: stat.size,
sizeHumanReadable: (stat.size / 1024).toFixed(1) + 'KiB',
isDirectory: stat.isDirectory(),
isFile: stat.isFile(),
});
},
], next);
}, callback);
}
uploadsController.uploadCategoryPicture = function (req, res, next) {
var uploadedFile = req.files.files[0];
var params = null;
@ -110,6 +203,25 @@ uploadsController.uploadSound = function (req, res, next) {
});
};
uploadsController.uploadFile = function (req, res, next) {
var uploadedFile = req.files.files[0];
var params;
try {
params = JSON.parse(req.body.params);
} catch (e) {
file.delete(uploadedFile.path);
return next(new Error('[[error:invalid-json]]'));
}
file.saveFileToLocal(uploadedFile.name, params.folder, uploadedFile.path, function (err, data) {
file.delete(uploadedFile.path);
if (err) {
return next(err);
}
res.json([{ url: data.url }]);
});
};
uploadsController.uploadDefaultAvatar = function (req, res, next) {
upload('avatar-default', req, res, next);
};
@ -173,3 +285,4 @@ function uploadImage(filename, folder, uploadedFile, req, res, next) {
res.json([{ name: uploadedFile.name, url: image.url.startsWith('http') ? image.url : nconf.get('relative_path') + image.url }]);
});
}

@ -11,6 +11,7 @@ var helpers = require('./helpers');
var Controllers = module.exports;
Controllers.ping = require('./ping');
Controllers.home = require('./home');
Controllers.topics = require('./topics');
Controllers.posts = require('./posts');

@ -0,0 +1,15 @@
'use strict';
var async = require('async');
var db = require('../database');
module.exports.ping = function (req, res, next) {
async.waterfall([
function (next) {
db.getObject('config', next);
},
function () {
res.status(200).send(req.path === '/sping' ? 'healthy' : '200');
},
], next);
};

@ -81,7 +81,7 @@ file.saveFileToLocal = function (filename, folder, tempPath, callback) {
}
callback(null, {
url: '/assets/uploads/' + folder + '/' + filename,
url: '/assets/uploads/' + (folder ? folder + '/' : '') + filename,
path: uploadPath,
});
});

@ -177,7 +177,7 @@ module.exports = function (middleware) {
var disabled = parseInt(meta.config.adminReloginDuration, 10) === 0;
if (disabled || (loginTime && parseInt(loginTime, 10) > Date.now() - adminReloginDuration)) {
var timeLeft = parseInt(loginTime, 10) - (Date.now() - adminReloginDuration);
if (timeLeft < Math.min(300000, adminReloginDuration)) {
if (req.session.meta && timeLeft < Math.min(300000, adminReloginDuration)) {
req.session.meta.datetime += Math.min(300000, adminReloginDuration);
}

@ -3,7 +3,7 @@
var qs = require('querystring');
var _ = require('lodash');
var pagination = {};
var pagination = module.exports;
pagination.create = function (currentPage, pageCount, queryObj) {
if (pageCount <= 1) {
@ -76,6 +76,3 @@ pagination.create = function (currentPage, pageCount, queryObj) {
}
return data;
};
module.exports = pagination;

@ -17,6 +17,7 @@ function apiRoutes(router, middleware, controllers) {
router.post('/uploadlogo', middlewares, controllers.admin.uploads.uploadLogo);
router.post('/uploadOgImage', middlewares, controllers.admin.uploads.uploadOgImage);
router.post('/upload/sound', middlewares, controllers.admin.uploads.uploadSound);
router.post('/upload/file', middlewares, controllers.admin.uploads.uploadFile);
router.post('/uploadDefaultAvatar', middlewares, controllers.admin.uploads.uploadDefaultAvatar);
}
@ -77,6 +78,8 @@ function addRoutes(router, middleware, controllers) {
router.get('/manage/groups', middlewares, controllers.admin.groups.list);
router.get('/manage/groups/:name', middlewares, controllers.admin.groups.get);
router.get('/manage/uploads', middlewares, controllers.admin.uploads.get);
router.get('/settings/:term?', middlewares, controllers.admin.settings.get);
router.get('/appearance/:term?', middlewares, controllers.admin.appearance.get);

@ -2,6 +2,9 @@
var async = require('async');
var winston = require('winston');
var fs = require('fs');
var path = require('path');
var nconf = require('nconf');
var meta = require('../meta');
var plugins = require('../plugins');
@ -37,6 +40,7 @@ var SocketAdmin = {
analytics: {},
logs: {},
errors: {},
uploads: {},
};
SocketAdmin.before = function (socket, method, data, next) {
@ -336,4 +340,13 @@ SocketAdmin.reloadAllSessions = function (socket, data, callback) {
callback();
};
SocketAdmin.uploads.delete = function (socket, pathToFile, callback) {
pathToFile = path.join(nconf.get('upload_path'), pathToFile);
if (!pathToFile.startsWith(nconf.get('upload_path'))) {
return callback(new Error('[[error:invalid-path]]'));
}
fs.unlink(pathToFile, callback);
};
module.exports = SocketAdmin;

@ -198,6 +198,7 @@ Topics.getTopicWithPosts = function (topicData, set, uid, start, stop, reverse,
bookmark: async.apply(Topics.getUserBookmark, topicData.tid, uid),
postSharing: async.apply(social.getActivePostSharing),
deleter: async.apply(getDeleter, topicData),
merger: async.apply(getMerger, topicData),
related: function (next) {
async.waterfall([
function (next) {
@ -223,6 +224,8 @@ Topics.getTopicWithPosts = function (topicData, set, uid, start, stop, reverse,
topicData.postSharing = results.postSharing;
topicData.deleter = results.deleter;
topicData.deletedTimestampISO = utils.toISOString(topicData.deletedTimestamp);
topicData.merger = results.merger;
topicData.mergedTimestampISO = utils.toISOString(topicData.mergedTimestamp);
topicData.related = results.related || [];
topicData.unreplied = parseInt(topicData.postcount, 10) === 1;
@ -290,6 +293,28 @@ function getDeleter(topicData, callback) {
user.getUserFields(topicData.deleterUid, ['username', 'userslug', 'picture'], callback);
}
function getMerger(topicData, callback) {
if (!topicData.mergerUid) {
return setImmediate(callback, null, null);
}
async.waterfall([
function (next) {
async.parallel({
merger: function (next) {
user.getUserFields(topicData.mergerUid, ['username', 'userslug', 'picture'], next);
},
mergedIntoTitle: function (next) {
Topics.getTopicField(topicData.mergeIntoTid, 'title', next);
},
}, next);
},
function (results, next) {
results.merger.mergedIntoTitle = results.mergedIntoTitle;
next(null, results.merger);
},
], callback);
}
Topics.getMainPost = function (tid, uid, callback) {
Topics.getMainPosts([tid], uid, function (err, mainPosts) {
callback(err, Array.isArray(mainPosts) && mainPosts.length ? mainPosts[0] : null);

@ -26,6 +26,13 @@ module.exports = function (Topics) {
function (next) {
Topics.delete(tid, uid, next);
},
function (next) {
Topics.setTopicFields(tid, {
mergeIntoTid: mergeIntoTid,
mergerUid: uid,
mergedTimestamp: Date.now(),
}, next);
},
], next);
}, callback);
};

@ -0,0 +1,39 @@
<!-- IMPORT partials/breadcrumbs.tpl -->
<div class="clearfix">
<button id="upload" class="btn-success pull-right"><i class="fa fa-upload"></i> [[global:upload]]</button>
</div>
<div class="table-responsive">
<table class="table table-striped users-table">
<thead>
<tr>
<th>[[admin/manage/uploads:filename]]</th>
<th class="text-right">[[admin/manage/uploads:size/filecount]]</th>
<th></th>
</tr>
</thead>
<tbody>
<!-- BEGIN files -->
<tr data-path="{files.path}">
<!-- IF files.isDirectory -->
<td class="col-md-9" role="button">
<i class="fa fa-fw fa-folder-o"></i> <a href="{config.relative}/admin/manage/uploads?dir={files.path}">{files.name}</a>
</td>
<!-- ENDIF files.isDirectory -->
<!-- IF files.isFile -->
<td class="col-md-9">
<i class="fa fa-fw fa-file-text-o"></i> <a href="{config.relative_path}{files.url}" target="_blank">{files.name}</a>
</td>
<!-- ENDIF files.isFile -->
<td class="col-md-2 text-right"><!-- IF files.size -->{files.sizeHumanReadable}<!-- ELSE -->[[admin/manage/uploads:filecount, {files.fileCount}]]<!-- ENDIF files.size --></td>
<td role="button" class="col-md-1 text-right"><i class="delete fa fa-fw fa-trash-o <!-- IF !files.isFile --> hidden<!-- ENDIF !files.isFile -->"></i></td>
</tr>
<!-- END files -->
</tbody>
</table>
</div>
<!-- IMPORT partials/paginator.tpl -->

@ -23,6 +23,7 @@
<li><a href="{relative_path}/admin/manage/registration">[[admin/menu:manage/registration]]</a></li>
<li><a href="{relative_path}/admin/manage/post-queue">[[admin/menu:manage/post-queue]]</a></li>
<li><a href="{relative_path}/admin/manage/ip-blacklist">[[admin/menu:manage/ip-blacklist]]</a></li>
<li><a href="{relative_path}/admin/manage/uploads">[[admin/menu:manage/uploads]]</a></li>
</ul>
</section>
@ -198,6 +199,7 @@
<li><a href="{relative_path}/admin/manage/registration">[[admin/menu:manage/registration]]</a></li>
<li><a href="{relative_path}/admin/manage/post-queue">[[admin/menu:manage/post-queue]]</a></li>
<li><a href="{relative_path}/admin/manage/ip-blacklist">[[admin/menu:manage/ip-blacklist]]</a></li>
<li><a href="{relative_path}/admin/manage/uploads">[[admin/menu:manage/uploads]]</a></li>
</ul>
</li>
<li class="dropdown menu-item">

@ -116,6 +116,7 @@ function initializeNodeBB(callback) {
function setupExpressApp(app, callback) {
var middleware = require('./middleware');
var pingController = require('./controllers/ping');
var relativePath = nconf.get('relative_path');
var viewsDir = nconf.get('views_dir');
@ -147,8 +148,8 @@ function setupExpressApp(app, callback) {
app.use(compression());
app.get(relativePath + '/ping', ping);
app.get(relativePath + '/sping', ping);
app.get(relativePath + '/ping', pingController.ping);
app.get(relativePath + '/sping', pingController.ping);
setupFavicon(app);
@ -179,17 +180,6 @@ function setupExpressApp(app, callback) {
setupAutoLocale(app, callback);
}
function ping(req, res, next) {
async.waterfall([
function (next) {
db.getObject('config', next);
},
function () {
res.status(200).send(req.path === '/sping' ? 'healthy' : '200');
},
], next);
}
function setupFavicon(app) {
var faviconPath = meta.config['brand:favicon'] || 'favicon.ico';
faviconPath = path.join(nconf.get('base_dir'), 'public', faviconPath.replace(/assets\/uploads/, 'uploads'));

@ -17,7 +17,7 @@ nconf.file({ file: path.join(__dirname, '../../config.json') });
nconf.defaults({
base_dir: path.join(__dirname, '../..'),
themes_path: path.join(__dirname, '../../node_modules'),
upload_path: 'public/uploads',
upload_path: 'test/uploads',
views_dir: path.join(__dirname, '../../build/public/templates'),
relative_path: '',
});
@ -173,6 +173,21 @@ function setupMockDefaults(callback) {
id: 'nodebb-theme-persona',
}, next);
},
function (next) {
var rimraf = require('rimraf');
rimraf('test/uploads', next);
},
function (next) {
var mkdirp = require('mkdirp');
async.eachSeries([
'test/uploads',
'test/uploads/category',
'test/uploads/files',
'test/uploads/system',
'test/uploads/sounds',
'test/uploads/profile',
], mkdirp, next);
},
], callback);
}
db.setupMockDefaults = setupMockDefaults;

@ -816,7 +816,7 @@ describe('User', function () {
}, function (err, uploadedPicture) {
assert.ifError(err);
assert.equal(uploadedPicture.url, '/assets/uploads/profile/' + uid + '-profileavatar.png');
assert.equal(uploadedPicture.path, path.join(nconf.get('base_dir'), 'public', 'uploads', 'profile', uid + '-profileavatar.png'));
assert.equal(uploadedPicture.path, path.join(nconf.get('upload_path'), 'profile', uid + '-profileavatar.png'));
done();
});
}

Loading…
Cancel
Save