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

This commit is contained in:
Barış Soner Uşaklı
2018-01-26 18:58:01 -05:00
35 changed files with 359 additions and 102 deletions

1
.gitignore vendored
View File

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

View File

@@ -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"

View File

@@ -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) {

View File

@@ -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"
}

View File

@@ -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",

View File

@@ -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.",

View File

@@ -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"

View File

@@ -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.",

View File

@@ -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.",

View File

@@ -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",

View File

@@ -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"
}

View File

@@ -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é !"
}

View File

@@ -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",

View File

@@ -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.",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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;
});

View File

@@ -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;
});

View File

@@ -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);
}
});

View File

@@ -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;

View File

@@ -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 }]);
});
}

View File

@@ -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');

15
src/controllers/ping.js Normal file
View File

@@ -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);
};

View File

@@ -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,
});
});

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);
};

View File

@@ -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 -->

View File

@@ -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">

View File

@@ -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'));

View File

@@ -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;

View File

@@ -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();
});
}