Merge branch 'master' into bookmark2

v1.18.x
bdharrington7 10 years ago
commit 2a82b5db91

13
.gitattributes vendored

@ -0,0 +1,13 @@
# These files are text and should be normalized (convert crlf => lf)
*.json text
*.css text
*.less text
*.tpl text
*.html text
*.js text
*.md text
# Images should be treated as binary
# (binary is a macro for -text -diff)
*.png binary
*.jpg binary

@ -1,75 +1,75 @@
# <img alt="NodeBB" src="http://i.imgur.com/mYxPPtB.png" />
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/NodeBB/NodeBB?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![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/NodeBB/NodeBB/badges/gpa.svg)](https://codeclimate.com/github/NodeBB/NodeBB)
[![Documentation Status](https://readthedocs.org/projects/nodebb/badge/?version=latest)](https://readthedocs.org/projects/nodebb/?badge=latest)
**NodeBB Forum Software** is powered by Node.js and built on either a Redis or MongoDB database. It utilizes web sockets for instant interactions and real-time notifications. NodeBB is compatible down to IE8 and has many modern features out of the box such as social network integration and streaming discussions.
Additional functionality is enabled through the use of third-party plugins.
* [Get NodeBB](http://www.nodebb.org/ "NodeBB")
* [Demo & Meta Discussion](http://community.nodebb.org)
* [Documentation & Installation Instructions](http://docs.nodebb.org)
* [Help translate NodeBB](https://www.transifex.com/projects/p/nodebb/)
* [NodeBB Blog](http://blog.nodebb.org)
* [Join us on IRC](https://kiwiirc.com/client/irc.freenode.net/nodebb) - #nodebb on Freenode
* [Follow us on Twitter](http://www.twitter.com/NodeBB/ "NodeBB Twitter")
* [Like us on Facebook](http://www.facebook.com/NodeBB/ "NodeBB Facebook")
## Screenshots
[![](http://i.imgur.com/VCoOFyqb.png)](http://i.imgur.com/VCoOFyq.png)
[![](http://i.imgur.com/FLOUuIqb.png)](http://i.imgur.com/FLOUuIq.png)
[![](http://i.imgur.com/Ud1LrfIb.png)](http://i.imgur.com/Ud1LrfI.png)
[![](http://i.imgur.com/h6yZ66sb.png)](http://i.imgur.com/h6yZ66s.png)
[![](http://i.imgur.com/o90kVPib.png)](http://i.imgur.com/o90kVPi.png)
[![](http://i.imgur.com/AaRRrU2b.png)](http://i.imgur.com/AaRRrU2.png)
[![](http://i.imgur.com/LmHtPhob.png)](http://i.imgur.com/LmHtPho.png)
[![](http://i.imgur.com/paiJPJkb.jpg)](http://i.imgur.com/paiJPJk.jpg)
[![](http://i.imgur.com/8OLssij.png)](http://i.imgur.com/8OLssij.png)
[![](http://i.imgur.com/JKOc0LZ.png)](http://i.imgur.com/JKOc0LZ.png)
## How can I follow along/contribute?
* Our feature roadmap is hosted on the project wiki's [Version History / Roadmap](https://github.com/NodeBB/NodeBB/wiki/Version-History-%26-Roadmap)
* If you are a developer, feel free to check out the source and submit pull requests. We also have a wide array of [plugins](http://community.nodebb.org/category/7/nodebb-plugins) which would be a great starting point for learning the codebase.
* If you are a designer, [NodeBB needs themes](http://community.nodebb.org/category/10/nodebb-themes)! NodeBB's theming system allows extention of the base templates as well as styling via LESS or CSS. NodeBB's base theme utilizes [Bootstrap 3](http://getbootstrap.com/) but themes can choose to use a different framework altogether.
* If you know languages other than English you can help us translate NodeBB. We use [Transifex](https://www.transifex.com/projects/p/nodebb/) for internationalization.
* Please don't forget to **like**, **follow**, and **star our repo**! Join our growing [community](http://community.nodebb.org) to keep up to date with the latest NodeBB development.
## Requirements
NodeBB requires the following software to be installed:
* A version of Node.js at least 0.10 or greater
* Redis, version 2.8.9 or greater **or** MongoDB, version 2.6 or greater
* nginx, version 1.3.13 or greater (**only if** intending to use nginx to proxy requests to a NodeBB)
## Installation
[Please refer to platform-specific installation documentation](http://docs.nodebb.org/en/latest/installing/os.html)
## Securing NodeBB
It is important to ensure that your NodeBB and database servers are secured. Bear these points in mind:
1. While some distributions set up Redis with a more restrictive configuration, Redis by default listens to all interfaces, which is especially dangerous when a server is open to the public. Some suggestions:
* Set `bind_address` to `127.0.0.1` so as to restrict access to the local machine only
* Use `requirepass` to secure Redis behind a password (preferably a long one)
* Familiarise yourself with [Redis Security](http://redis.io/topics/security)
2. Use `iptables` to secure your server from unintended open ports. In Ubuntu, `ufw` provides a friendlier interface to working with `iptables`.
* e.g. If your NodeBB is proxied, no ports should be open except 80 (and possibly 22, for SSH access)
## Upgrading NodeBB
Detailed upgrade instructions are listed in [Upgrading NodeBB](https://docs.nodebb.org/en/latest/upgrading/index.html)
## License
NodeBB is licensed under the **GNU General Public License v3 (GPL-3)** (http://www.gnu.org/copyleft/gpl.html).
# <img alt="NodeBB" src="http://i.imgur.com/mYxPPtB.png" />
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/NodeBB/NodeBB?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![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/NodeBB/NodeBB/badges/gpa.svg)](https://codeclimate.com/github/NodeBB/NodeBB)
[![Documentation Status](https://readthedocs.org/projects/nodebb/badge/?version=latest)](https://readthedocs.org/projects/nodebb/?badge=latest)
**NodeBB Forum Software** is powered by Node.js and built on either a Redis or MongoDB database. It utilizes web sockets for instant interactions and real-time notifications. NodeBB is compatible down to IE8 and has many modern features out of the box such as social network integration and streaming discussions.
Additional functionality is enabled through the use of third-party plugins.
* [Get NodeBB](http://www.nodebb.org/ "NodeBB")
* [Demo & Meta Discussion](http://community.nodebb.org)
* [Documentation & Installation Instructions](http://docs.nodebb.org)
* [Help translate NodeBB](https://www.transifex.com/projects/p/nodebb/)
* [NodeBB Blog](http://blog.nodebb.org)
* [Join us on IRC](https://kiwiirc.com/client/irc.freenode.net/nodebb) - #nodebb on Freenode
* [Follow us on Twitter](http://www.twitter.com/NodeBB/ "NodeBB Twitter")
* [Like us on Facebook](http://www.facebook.com/NodeBB/ "NodeBB Facebook")
## Screenshots
[![](http://i.imgur.com/VCoOFyqb.png)](http://i.imgur.com/VCoOFyq.png)
[![](http://i.imgur.com/FLOUuIqb.png)](http://i.imgur.com/FLOUuIq.png)
[![](http://i.imgur.com/Ud1LrfIb.png)](http://i.imgur.com/Ud1LrfI.png)
[![](http://i.imgur.com/h6yZ66sb.png)](http://i.imgur.com/h6yZ66s.png)
[![](http://i.imgur.com/o90kVPib.png)](http://i.imgur.com/o90kVPi.png)
[![](http://i.imgur.com/AaRRrU2b.png)](http://i.imgur.com/AaRRrU2.png)
[![](http://i.imgur.com/LmHtPhob.png)](http://i.imgur.com/LmHtPho.png)
[![](http://i.imgur.com/paiJPJkb.jpg)](http://i.imgur.com/paiJPJk.jpg)
[![](http://i.imgur.com/8OLssij.png)](http://i.imgur.com/8OLssij.png)
[![](http://i.imgur.com/JKOc0LZ.png)](http://i.imgur.com/JKOc0LZ.png)
## How can I follow along/contribute?
* Our feature roadmap is hosted on the project wiki's [Version History / Roadmap](https://github.com/NodeBB/NodeBB/wiki/Version-History-%26-Roadmap)
* If you are a developer, feel free to check out the source and submit pull requests. We also have a wide array of [plugins](http://community.nodebb.org/category/7/nodebb-plugins) which would be a great starting point for learning the codebase.
* If you are a designer, [NodeBB needs themes](http://community.nodebb.org/category/10/nodebb-themes)! NodeBB's theming system allows extention of the base templates as well as styling via LESS or CSS. NodeBB's base theme utilizes [Bootstrap 3](http://getbootstrap.com/) but themes can choose to use a different framework altogether.
* If you know languages other than English you can help us translate NodeBB. We use [Transifex](https://www.transifex.com/projects/p/nodebb/) for internationalization.
* Please don't forget to **like**, **follow**, and **star our repo**! Join our growing [community](http://community.nodebb.org) to keep up to date with the latest NodeBB development.
## Requirements
NodeBB requires the following software to be installed:
* A version of Node.js at least 0.10 or greater
* Redis, version 2.8.9 or greater **or** MongoDB, version 2.6 or greater
* nginx, version 1.3.13 or greater (**only if** intending to use nginx to proxy requests to a NodeBB)
## Installation
[Please refer to platform-specific installation documentation](http://docs.nodebb.org/en/latest/installing/os.html)
## Securing NodeBB
It is important to ensure that your NodeBB and database servers are secured. Bear these points in mind:
1. While some distributions set up Redis with a more restrictive configuration, Redis by default listens to all interfaces, which is especially dangerous when a server is open to the public. Some suggestions:
* Set `bind_address` to `127.0.0.1` so as to restrict access to the local machine only
* Use `requirepass` to secure Redis behind a password (preferably a long one)
* Familiarise yourself with [Redis Security](http://redis.io/topics/security)
2. Use `iptables` to secure your server from unintended open ports. In Ubuntu, `ufw` provides a friendlier interface to working with `iptables`.
* e.g. If your NodeBB is proxied, no ports should be open except 80 (and possibly 22, for SSH access)
## Upgrading NodeBB
Detailed upgrade instructions are listed in [Upgrading NodeBB](https://docs.nodebb.org/en/latest/upgrading/index.html)
## License
NodeBB is licensed under the **GNU General Public License v3 (GPL-3)** (http://www.gnu.org/copyleft/gpl.html).
Interested in a sublicense agreement for use of NodeBB in a non-free/restrictive environment? Contact us at sales@nodebb.org.

832
app.js

@ -1,416 +1,416 @@
/*
NodeBB - A better forum platform for the modern web
https://github.com/NodeBB/NodeBB/
Copyright (C) 2013-2014 NodeBB Inc.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
/*global require, global, process*/
var nconf = require('nconf');
nconf.argv().env('__');
var fs = require('fs'),
os = require('os'),
url = require('url'),
async = require('async'),
semver = require('semver'),
winston = require('winston'),
colors = require('colors'),
path = require('path'),
pkg = require('./package.json'),
utils = require('./public/src/utils.js');
global.env = process.env.NODE_ENV || 'production';
winston.remove(winston.transports.Console);
winston.add(winston.transports.Console, {
colorize: true,
timestamp: function() {
var date = new Date();
return date.getDate() + '/' + (date.getMonth() + 1) + ' ' + date.toTimeString().substr(0,5) + ' [' + global.process.pid + ']';
},
level: nconf.get('log-level') || (global.env === 'production' ? 'info' : 'verbose')
});
if(os.platform() === 'linux') {
require('child_process').exec('/usr/bin/which convert', function(err, stdout, stderr) {
if(err || !stdout) {
winston.warn('Couldn\'t find convert. Did you install imagemagick?');
}
});
}
// Alternate configuration file support
var configFile = path.join(__dirname, '/config.json'),
configExists;
if (nconf.get('config')) {
configFile = path.resolve(__dirname, nconf.get('config'));
}
configExists = fs.existsSync(configFile);
if (!nconf.get('setup') && !nconf.get('install') && !nconf.get('upgrade') && !nconf.get('reset') && configExists) {
start();
} else if (nconf.get('setup') || nconf.get('install')) {
setup();
} else if (!configExists) {
require('./install/web').install(nconf.get('port'));
} else if (nconf.get('upgrade')) {
upgrade();
} else if (nconf.get('reset')) {
reset();
}
function loadConfig() {
nconf.file({
file: configFile
});
nconf.defaults({
base_dir: __dirname,
themes_path: path.join(__dirname, 'node_modules'),
views_dir: path.join(__dirname, 'public/templates'),
version: pkg.version
});
if (!nconf.get('isCluster')) {
nconf.set('isPrimary', 'true');
nconf.set('isCluster', 'false');
}
// Ensure themes_path is a full filepath
nconf.set('themes_path', path.resolve(__dirname, nconf.get('themes_path')));
nconf.set('core_templates_path', path.join(__dirname, 'src/views'));
nconf.set('base_templates_path', path.join(nconf.get('themes_path'), 'nodebb-theme-vanilla/templates'));
if (!process.send) {
// If run using `node app`, log GNU copyright info along with server info
winston.info('NodeBB v' + nconf.get('version') + ' Copyright (C) 2013-2014 NodeBB Inc.');
winston.info('This program comes with ABSOLUTELY NO WARRANTY.');
winston.info('This is free software, and you are welcome to redistribute it under certain conditions.');
winston.info('');
}
}
function start() {
loadConfig();
var db = require('./src/database');
// nconf defaults, if not set in config
if (!nconf.get('upload_path')) {
nconf.set('upload_path', '/public/uploads');
}
// Parse out the relative_url and other goodies from the configured URL
var urlObject = url.parse(nconf.get('url'));
var relativePath = urlObject.pathname !== '/' ? urlObject.pathname : '';
nconf.set('base_url', urlObject.protocol + '//' + urlObject.host);
nconf.set('use_port', !!urlObject.port);
nconf.set('relative_path', relativePath);
nconf.set('port', urlObject.port || nconf.get('port') || nconf.get('PORT') || 4567);
nconf.set('upload_url', '/uploads/');
if (nconf.get('isPrimary') === 'true') {
winston.info('Time: %s', (new Date()).toString());
winston.info('Initializing NodeBB v%s', nconf.get('version'));
winston.verbose('* using configuration stored in: %s', configFile);
var host = nconf.get(nconf.get('database') + ':host'),
storeLocation = host ? 'at ' + host + (host.indexOf('/') === -1 ? ':' + nconf.get(nconf.get('database') + ':port') : '') : '';
winston.verbose('* using %s store %s', nconf.get('database'), storeLocation);
winston.verbose('* using themes stored in: %s', nconf.get('themes_path'));
}
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
process.on('SIGHUP', restart);
process.on('message', function(message) {
if (typeof message !== 'object') {
return;
}
var meta = require('./src/meta');
var emitter = require('./src/emitter');
switch (message.action) {
case 'reload':
meta.reload();
break;
case 'js-propagate':
meta.js.cache = message.cache;
meta.js.map = message.map;
meta.js.hash = message.hash;
emitter.emit('meta:js.compiled');
winston.verbose('[cluster] Client-side javascript and mapping propagated to worker %s', process.pid);
break;
case 'css-propagate':
meta.css.cache = message.cache;
meta.css.acpCache = message.acpCache;
meta.css.hash = message.hash;
emitter.emit('meta:css.compiled');
winston.verbose('[cluster] Stylesheets propagated to worker %s', process.pid);
break;
case 'templates:compiled':
emitter.emit('templates:compiled');
break;
}
});
process.on('uncaughtException', function(err) {
winston.error(err.stack);
console.log(err.stack);
require('./src/meta').js.killMinifier();
shutdown(1);
});
async.waterfall([
async.apply(db.init),
async.apply(db.checkCompatibility),
function(next) {
require('./src/meta').configs.init(next);
},
function(next) {
require('./src/meta').dependencies.check(next);
},
function(next) {
require('./src/upgrade').check(next);
},
function(next) {
var webserver = require('./src/webserver');
require('./src/socket.io').init(webserver.server);
if (nconf.get('isPrimary') === 'true' && !nconf.get('jobsDisabled')) {
require('./src/notifications').init();
require('./src/user').startJobs();
}
webserver.listen();
}
], function(err) {
if (err) {
switch(err.message) {
case 'schema-out-of-date':
winston.warn('Your NodeBB schema is out-of-date. Please run the following command to bring your dataset up to spec:');
winston.warn(' ./nodebb upgrade');
break;
case 'dependencies-out-of-date':
winston.warn('One or more of NodeBB\'s dependent packages are out-of-date. Please run the following command to update them:');
winston.warn(' ./nodebb upgrade');
break;
default:
if (err.stacktrace !== false) {
winston.error(err.stack);
} else {
winston.error(err.message);
}
break;
}
// Either way, bad stuff happened. Abort start.
process.exit();
}
});
}
function setup() {
loadConfig();
winston.info('NodeBB Setup Triggered via Command Line');
var install = require('./src/install');
process.stdout.write('\nWelcome to NodeBB!\n');
process.stdout.write('\nThis looks like a new installation, so you\'ll have to answer a few questions about your environment before we can proceed.\n');
process.stdout.write('Press enter to accept the default setting (shown in brackets).\n');
install.setup(function (err, data) {
var separator = ' ';
if (process.stdout.columns > 10) {
for(var x=0,cols=process.stdout.columns-10;x<cols;x++) {
separator += '=';
}
}
process.stdout.write('\n' + separator + '\n\n');
if (err) {
winston.error('There was a problem completing NodeBB setup: ', err.message);
} else {
if (data.hasOwnProperty('password')) {
process.stdout.write('An administrative user was automatically created for you:\n');
process.stdout.write(' Username: ' + data.username + '\n');
process.stdout.write(' Password: ' + data.password + '\n');
process.stdout.write('\n');
}
process.stdout.write('NodeBB Setup Completed. Run \'./nodebb start\' to manually start your NodeBB server.\n');
// If I am a child process, notify the parent of the returned data before exiting (useful for notifying
// hosts of auto-generated username/password during headless setups)
if (process.send) {
process.send(data);
}
}
process.exit();
});
}
function upgrade() {
loadConfig();
require('./src/database').init(function(err) {
if (err) {
winston.error(err.stack);
process.exit();
}
require('./src/meta').configs.init(function () {
require('./src/upgrade').upgrade();
});
});
}
function reset() {
loadConfig();
require('./src/database').init(function(err) {
if (err) {
winston.error(err.message);
process.exit();
}
if (nconf.get('t')) {
resetThemes();
} else if (nconf.get('p')) {
if (nconf.get('p') === true) {
resetPlugins();
} else {
resetPlugin(nconf.get('p'));
}
} else if (nconf.get('w')) {
resetWidgets();
} else if (nconf.get('s')) {
resetSettings();
} else if (nconf.get('a')) {
require('async').series([resetWidgets, resetThemes, resetPlugins, resetSettings], function(err) {
if (!err) {
winston.info('[reset] Reset complete.');
} else {
winston.error('[reset] Errors were encountered while resetting your forum settings: %s', err.message);
}
process.exit();
});
} else {
process.stdout.write('\nNodeBB Reset\n'.bold);
process.stdout.write('No arguments passed in, so nothing was reset.\n\n'.yellow);
process.stdout.write('Use ./nodebb reset ' + '{-t|-p|-w|-s|-a}\n'.red);
process.stdout.write(' -t\tthemes\n');
process.stdout.write(' -p\tplugins\n');
process.stdout.write(' -w\twidgets\n');
process.stdout.write(' -s\tsettings\n');
process.stdout.write(' -a\tall of the above\n');
process.stdout.write('\nPlugin reset flag (-p) can take a single argument\n');
process.stdout.write(' e.g. ./nodebb reset -p nodebb-plugin-mentions\n');
process.exit();
}
});
}
function resetSettings(callback) {
var meta = require('./src/meta');
meta.configs.set('allowLocalLogin', 1, function(err) {
winston.info('[reset] Settings reset to default');
if (typeof callback === 'function') {
callback(err);
} else {
process.exit();
}
});
}
function resetThemes(callback) {
var meta = require('./src/meta');
meta.themes.set({
type: 'local',
id: 'nodebb-theme-vanilla'
}, function(err) {
winston.info('[reset] Theme reset to Vanilla');
if (typeof callback === 'function') {
callback(err);
} else {
process.exit();
}
});
}
function resetPlugin(pluginId) {
var db = require('./src/database');
db.sortedSetRemove('plugins:active', pluginId, function(err) {
if (err) {
winston.error('[reset] Could not disable plugin: %s encountered error %s', pluginId, err.message);
} else {
winston.info('[reset] Plugin `%s` disabled', pluginId);
}
process.exit();
});
}
function resetPlugins(callback) {
var db = require('./src/database');
db.delete('plugins:active', function(err) {
winston.info('[reset] All Plugins De-activated');
if (typeof callback === 'function') {
callback(err);
} else {
process.exit();
}
});
}
function resetWidgets(callback) {
require('./src/widgets').reset(function(err) {
winston.info('[reset] All Widgets moved to Draft Zone');
if (typeof callback === 'function') {
callback(err);
} else {
process.exit();
}
});
}
function shutdown(code) {
winston.info('[app] Shutdown (SIGTERM/SIGINT) Initialised.');
require('./src/database').close();
winston.info('[app] Database connection closed.');
require('./src/webserver').server.close();
winston.info('[app] Web server closed to connections.');
winston.info('[app] Shutdown complete.');
process.exit(code || 0);
}
function restart() {
if (process.send) {
winston.info('[app] Restarting...');
process.send({
action: 'restart'
});
} else {
winston.error('[app] Could not restart server. Shutting down.');
shutdown(1);
}
}
/*
NodeBB - A better forum platform for the modern web
https://github.com/NodeBB/NodeBB/
Copyright (C) 2013-2014 NodeBB Inc.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
/*global require, global, process*/
var nconf = require('nconf');
nconf.argv().env('__');
var fs = require('fs'),
os = require('os'),
url = require('url'),
async = require('async'),
semver = require('semver'),
winston = require('winston'),
colors = require('colors'),
path = require('path'),
pkg = require('./package.json'),
utils = require('./public/src/utils.js');
global.env = process.env.NODE_ENV || 'production';
winston.remove(winston.transports.Console);
winston.add(winston.transports.Console, {
colorize: true,
timestamp: function() {
var date = new Date();
return date.getDate() + '/' + (date.getMonth() + 1) + ' ' + date.toTimeString().substr(0,5) + ' [' + global.process.pid + ']';
},
level: nconf.get('log-level') || (global.env === 'production' ? 'info' : 'verbose')
});
if(os.platform() === 'linux') {
require('child_process').exec('/usr/bin/which convert', function(err, stdout, stderr) {
if(err || !stdout) {
winston.warn('Couldn\'t find convert. Did you install imagemagick?');
}
});
}
// Alternate configuration file support
var configFile = path.join(__dirname, '/config.json'),
configExists;
if (nconf.get('config')) {
configFile = path.resolve(__dirname, nconf.get('config'));
}
configExists = fs.existsSync(configFile);
if (!nconf.get('setup') && !nconf.get('install') && !nconf.get('upgrade') && !nconf.get('reset') && configExists) {
start();
} else if (nconf.get('setup') || nconf.get('install')) {
setup();
} else if (!configExists) {
require('./install/web').install(nconf.get('port'));
} else if (nconf.get('upgrade')) {
upgrade();
} else if (nconf.get('reset')) {
reset();
}
function loadConfig() {
nconf.file({
file: configFile
});
nconf.defaults({
base_dir: __dirname,
themes_path: path.join(__dirname, 'node_modules'),
views_dir: path.join(__dirname, 'public/templates'),
version: pkg.version
});
if (!nconf.get('isCluster')) {
nconf.set('isPrimary', 'true');
nconf.set('isCluster', 'false');
}
// Ensure themes_path is a full filepath
nconf.set('themes_path', path.resolve(__dirname, nconf.get('themes_path')));
nconf.set('core_templates_path', path.join(__dirname, 'src/views'));
nconf.set('base_templates_path', path.join(nconf.get('themes_path'), 'nodebb-theme-vanilla/templates'));
if (!process.send) {
// If run using `node app`, log GNU copyright info along with server info
winston.info('NodeBB v' + nconf.get('version') + ' Copyright (C) 2013-2014 NodeBB Inc.');
winston.info('This program comes with ABSOLUTELY NO WARRANTY.');
winston.info('This is free software, and you are welcome to redistribute it under certain conditions.');
winston.info('');
}
}
function start() {
loadConfig();
var db = require('./src/database');
// nconf defaults, if not set in config
if (!nconf.get('upload_path')) {
nconf.set('upload_path', '/public/uploads');
}
// Parse out the relative_url and other goodies from the configured URL
var urlObject = url.parse(nconf.get('url'));
var relativePath = urlObject.pathname !== '/' ? urlObject.pathname : '';
nconf.set('base_url', urlObject.protocol + '//' + urlObject.host);
nconf.set('use_port', !!urlObject.port);
nconf.set('relative_path', relativePath);
nconf.set('port', urlObject.port || nconf.get('port') || nconf.get('PORT') || 4567);
nconf.set('upload_url', '/uploads/');
if (nconf.get('isPrimary') === 'true') {
winston.info('Time: %s', (new Date()).toString());
winston.info('Initializing NodeBB v%s', nconf.get('version'));
winston.verbose('* using configuration stored in: %s', configFile);
var host = nconf.get(nconf.get('database') + ':host'),
storeLocation = host ? 'at ' + host + (host.indexOf('/') === -1 ? ':' + nconf.get(nconf.get('database') + ':port') : '') : '';
winston.verbose('* using %s store %s', nconf.get('database'), storeLocation);
winston.verbose('* using themes stored in: %s', nconf.get('themes_path'));
}
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
process.on('SIGHUP', restart);
process.on('message', function(message) {
if (typeof message !== 'object') {
return;
}
var meta = require('./src/meta');
var emitter = require('./src/emitter');
switch (message.action) {
case 'reload':
meta.reload();
break;
case 'js-propagate':
meta.js.cache = message.cache;
meta.js.map = message.map;
meta.js.hash = message.hash;
emitter.emit('meta:js.compiled');
winston.verbose('[cluster] Client-side javascript and mapping propagated to worker %s', process.pid);
break;
case 'css-propagate':
meta.css.cache = message.cache;
meta.css.acpCache = message.acpCache;
meta.css.hash = message.hash;
emitter.emit('meta:css.compiled');
winston.verbose('[cluster] Stylesheets propagated to worker %s', process.pid);
break;
case 'templates:compiled':
emitter.emit('templates:compiled');
break;
}
});
process.on('uncaughtException', function(err) {
winston.error(err.stack);
console.log(err.stack);
require('./src/meta').js.killMinifier();
shutdown(1);
});
async.waterfall([
async.apply(db.init),
async.apply(db.checkCompatibility),
function(next) {
require('./src/meta').configs.init(next);
},
function(next) {
require('./src/meta').dependencies.check(next);
},
function(next) {
require('./src/upgrade').check(next);
},
function(next) {
var webserver = require('./src/webserver');
require('./src/socket.io').init(webserver.server);
if (nconf.get('isPrimary') === 'true' && !nconf.get('jobsDisabled')) {
require('./src/notifications').init();
require('./src/user').startJobs();
}
webserver.listen();
}
], function(err) {
if (err) {
switch(err.message) {
case 'schema-out-of-date':
winston.warn('Your NodeBB schema is out-of-date. Please run the following command to bring your dataset up to spec:');
winston.warn(' ./nodebb upgrade');
break;
case 'dependencies-out-of-date':
winston.warn('One or more of NodeBB\'s dependent packages are out-of-date. Please run the following command to update them:');
winston.warn(' ./nodebb upgrade');
break;
default:
if (err.stacktrace !== false) {
winston.error(err.stack);
} else {
winston.error(err.message);
}
break;
}
// Either way, bad stuff happened. Abort start.
process.exit();
}
});
}
function setup() {
loadConfig();
winston.info('NodeBB Setup Triggered via Command Line');
var install = require('./src/install');
process.stdout.write('\nWelcome to NodeBB!\n');
process.stdout.write('\nThis looks like a new installation, so you\'ll have to answer a few questions about your environment before we can proceed.\n');
process.stdout.write('Press enter to accept the default setting (shown in brackets).\n');
install.setup(function (err, data) {
var separator = ' ';
if (process.stdout.columns > 10) {
for(var x=0,cols=process.stdout.columns-10;x<cols;x++) {
separator += '=';
}
}
process.stdout.write('\n' + separator + '\n\n');
if (err) {
winston.error('There was a problem completing NodeBB setup: ', err.message);
} else {
if (data.hasOwnProperty('password')) {
process.stdout.write('An administrative user was automatically created for you:\n');
process.stdout.write(' Username: ' + data.username + '\n');
process.stdout.write(' Password: ' + data.password + '\n');
process.stdout.write('\n');
}
process.stdout.write('NodeBB Setup Completed. Run \'./nodebb start\' to manually start your NodeBB server.\n');
// If I am a child process, notify the parent of the returned data before exiting (useful for notifying
// hosts of auto-generated username/password during headless setups)
if (process.send) {
process.send(data);
}
}
process.exit();
});
}
function upgrade() {
loadConfig();
require('./src/database').init(function(err) {
if (err) {
winston.error(err.stack);
process.exit();
}
require('./src/meta').configs.init(function () {
require('./src/upgrade').upgrade();
});
});
}
function reset() {
loadConfig();
require('./src/database').init(function(err) {
if (err) {
winston.error(err.message);
process.exit();
}
if (nconf.get('t')) {
resetThemes();
} else if (nconf.get('p')) {
if (nconf.get('p') === true) {
resetPlugins();
} else {
resetPlugin(nconf.get('p'));
}
} else if (nconf.get('w')) {
resetWidgets();
} else if (nconf.get('s')) {
resetSettings();
} else if (nconf.get('a')) {
require('async').series([resetWidgets, resetThemes, resetPlugins, resetSettings], function(err) {
if (!err) {
winston.info('[reset] Reset complete.');
} else {
winston.error('[reset] Errors were encountered while resetting your forum settings: %s', err.message);
}
process.exit();
});
} else {
process.stdout.write('\nNodeBB Reset\n'.bold);
process.stdout.write('No arguments passed in, so nothing was reset.\n\n'.yellow);
process.stdout.write('Use ./nodebb reset ' + '{-t|-p|-w|-s|-a}\n'.red);
process.stdout.write(' -t\tthemes\n');
process.stdout.write(' -p\tplugins\n');
process.stdout.write(' -w\twidgets\n');
process.stdout.write(' -s\tsettings\n');
process.stdout.write(' -a\tall of the above\n');
process.stdout.write('\nPlugin reset flag (-p) can take a single argument\n');
process.stdout.write(' e.g. ./nodebb reset -p nodebb-plugin-mentions\n');
process.exit();
}
});
}
function resetSettings(callback) {
var meta = require('./src/meta');
meta.configs.set('allowLocalLogin', 1, function(err) {
winston.info('[reset] Settings reset to default');
if (typeof callback === 'function') {
callback(err);
} else {
process.exit();
}
});
}
function resetThemes(callback) {
var meta = require('./src/meta');
meta.themes.set({
type: 'local',
id: 'nodebb-theme-vanilla'
}, function(err) {
winston.info('[reset] Theme reset to Vanilla');
if (typeof callback === 'function') {
callback(err);
} else {
process.exit();
}
});
}
function resetPlugin(pluginId) {
var db = require('./src/database');
db.sortedSetRemove('plugins:active', pluginId, function(err) {
if (err) {
winston.error('[reset] Could not disable plugin: %s encountered error %s', pluginId, err.message);
} else {
winston.info('[reset] Plugin `%s` disabled', pluginId);
}
process.exit();
});
}
function resetPlugins(callback) {
var db = require('./src/database');
db.delete('plugins:active', function(err) {
winston.info('[reset] All Plugins De-activated');
if (typeof callback === 'function') {
callback(err);
} else {
process.exit();
}
});
}
function resetWidgets(callback) {
require('./src/widgets').reset(function(err) {
winston.info('[reset] All Widgets moved to Draft Zone');
if (typeof callback === 'function') {
callback(err);
} else {
process.exit();
}
});
}
function shutdown(code) {
winston.info('[app] Shutdown (SIGTERM/SIGINT) Initialised.');
require('./src/database').close();
winston.info('[app] Database connection closed.');
require('./src/webserver').server.close();
winston.info('[app] Web server closed to connections.');
winston.info('[app] Shutdown complete.');
process.exit(code || 0);
}
function restart() {
if (process.send) {
winston.info('[app] Restarting...');
process.send({
action: 'restart'
});
} else {
winston.error('[app] Could not restart server. Shutting down.');
shutdown(1);
}
}

@ -1,34 +1,34 @@
[
{
"name": "Announcements",
"description": "Announcements regarding our community",
"bgColor": "#fda34b",
"color": "#fff",
"icon" : "fa-bullhorn",
"order": 1
},
{
"name": "General Discussion",
"description": "A place to talk about whatever you want",
"bgColor": "#59b3d0",
"color": "#fff",
"icon" : "fa-comments-o",
"order": 2
},
{
"name": "Blogs",
"description": "Blog posts from individual members",
"bgColor": "#86ba4b",
"color": "#fff",
"icon" : "fa-newspaper-o",
"order": 4
},
{
"name": "Comments & Feedback",
"description": "Got a question? Ask away!",
"bgColor": "#e95c5a",
"color": "#fff",
"icon" : "fa-question",
"order": 3
}
[
{
"name": "Announcements",
"description": "Announcements regarding our community",
"bgColor": "#fda34b",
"color": "#fff",
"icon" : "fa-bullhorn",
"order": 1
},
{
"name": "General Discussion",
"description": "A place to talk about whatever you want",
"bgColor": "#59b3d0",
"color": "#fff",
"icon" : "fa-comments-o",
"order": 2
},
{
"name": "Blogs",
"description": "Blog posts from individual members",
"bgColor": "#86ba4b",
"color": "#fff",
"icon" : "fa-newspaper-o",
"order": 4
},
{
"name": "Comments & Feedback",
"description": "Got a question? Ask away!",
"bgColor": "#e95c5a",
"color": "#fff",
"icon" : "fa-question",
"order": 3
}
]

@ -2,7 +2,7 @@
"name": "nodebb",
"license": "GPL-3.0",
"description": "NodeBB Forum",
"version": "0.7.1-dev",
"version": "0.7.2-dev",
"homepage": "http://www.nodebb.org",
"repository": {
"type": "git",

@ -1,15 +1,15 @@
{
"password-reset-requested": "Zurücksetzung des Passworts beantragt - %1!",
"welcome-to": "Willkommen bei %1",
"invite": "Invitation from %1",
"invite": "Einladung von %1",
"greeting_no_name": "Hallo",
"greeting_with_name": "Hallo %1",
"welcome.text1": "Vielen Dank für die Registrierung bei %1!",
"welcome.text2": "Um dein Konto vollständig zu aktivieren, müssen wir überprüfen, ob du Besitzer der E-Mail-Adresse bist, mit der du dich registriert hast.",
"welcome.text3": "An administrator has accepted your registration application. You can login with your username/password now.",
"welcome.text3": "Ein Administrator hat deine Registration aktzeptiert. Du kannst dich jetzt mit deinem Benutzernamen/Passwort einloggen.",
"welcome.cta": "Klicke hier, um deine E-Mail-Adresse zu bestätigen.",
"invitation.text1": "%1 has invited you to join %2",
"invitation.ctr": "Click here to create your account.",
"invitation.text1": "%1 hat dich eingeladen %2 beizutreten",
"invitation.ctr": "Klicke hier, um ein Konto zu erstellen.",
"reset.text1": "Wir haben eine Anfrage auf Zurücksetzung deines Passworts erhalten, wahrscheinlich, weil du es vergessen hast. Falls dies nicht der Fall ist, ignoriere bitte diese E-Mail.",
"reset.text2": "Klicke bitte auf den folgenden Link, um mit der Zurücksetzung deines Passworts fortzufahren:",
"reset.cta": "Klicke hier, um dein Passwort zurückzusetzen",

@ -51,7 +51,7 @@
"already-favourited": "Dieser Beitrag ist bereits in deinen Favoriten enthalten",
"already-unfavourited": "Du hast diesen Beitrag bereits aus deinen Favoriten entfernt",
"cant-ban-other-admins": "Du kannst andere Administratoren nicht sperren!",
"cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin",
"cant-remove-last-admin": "Du bist der einzige Administrator. Füge zuerst einen anderen Administrator hinzu, bevor du dich selbst als Administrator entfernst",
"invalid-image-type": "Falsche Bildart. Erlaubte Arten sind: %1",
"invalid-image-extension": "Ungültige Dateinamenerweiterung",
"invalid-file-type": "Ungültiger Dateityp. Erlaubte Typen sind: %1",
@ -60,8 +60,8 @@
"group-name-change-not-allowed": "Du kannst den Namen der Gruppe nicht ändern",
"group-already-member": "Du bist bereits Teil dieser Gruppe",
"group-needs-owner": "Diese Gruppe muss mindestens einen Besitzer vorweisen",
"group-already-invited": "This user has already been invited",
"group-already-requested": "Your membership request has already been submitted",
"group-already-invited": "Dieser Benutzer wurde bereits eingeladen",
"group-already-requested": "Deine Mitgliedsanfrage wurde bereits eingereicht",
"post-already-deleted": "Dieser Beitrag ist bereits gelöscht worden",
"post-already-restored": "Dieser Beitrag ist bereits wiederhergestellt worden",
"topic-already-deleted": "Dieses Thema ist bereits gelöscht worden",
@ -79,7 +79,7 @@
"downvoting-disabled": "Downvotes sind deaktiviert.",
"not-enough-reputation-to-downvote": "Deine Reputation ist zu niedrig, um diesen Beitrag negativ zu bewerten.",
"not-enough-reputation-to-flag": "Deine Reputation ist nicht gut genug, um diesen Beitrag zu melden",
"already-flagged": "You have already flagged this post",
"already-flagged": "Du hast diesen Beitrag bereits gemeldet",
"reload-failed": "Es ist ein Problem während des Reloads von NodeBB aufgetreten: \"%1\". NodeBB wird weiterhin clientseitige Assets bereitstellen, allerdings solltest du das, was du vor dem Reload gemacht hast, rückgängig machen.",
"registration-error": "Registrierungsfehler",
"parse-error": "Beim auswerten der Serverantwort ist etwas schiefgegangen",

@ -50,7 +50,7 @@
"views": "Aufrufe",
"reputation": "Reputation",
"read_more": "weiterlesen",
"more": "More",
"more": "Mehr",
"posted_ago_by_guest": "%1 von einem Gast geschrieben",
"posted_ago_by": "%1 von %2 geschrieben",
"posted_ago": "%1 geschrieben",

@ -6,12 +6,12 @@
"no_groups_found": "Es sind keine Gruppen vorhanden",
"pending.accept": "Annehmen",
"pending.reject": "Abweisen",
"pending.accept_all": "Accept All",
"pending.reject_all": "Reject All",
"pending.none": "There are no pending members at this time",
"invited.none": "There are no invited members at this time",
"invited.uninvite": "Rescind Invitation",
"invited.search": "Search for a user to invite to this group",
"pending.accept_all": "Alle annehmen",
"pending.reject_all": "Alle ablehnen",
"pending.none": "Es sind zur Zeit keine unvearbeiteten Mitglieder vorhanden",
"invited.none": "Es sind zur Zeit keine weiteren Mitglieder eingeladen",
"invited.uninvite": "Einladung zurücknehmen",
"invited.search": "Suche nach einem Benutzer um ihn in diese Gruppe aufzunehmen",
"cover-instructions": "Foto auf eine Position bewegen, und <strong>Speichern</strong> drücken",
"cover-change": "Ändern",
"cover-save": "Speichern",
@ -19,7 +19,7 @@
"details.title": "Gruppendetails",
"details.members": "Mitgliederliste",
"details.pending": "Mitglieder in Schwebe",
"details.invited": "Invited Members",
"details.invited": "Eingeladene Mitglieder",
"details.has_no_posts": "Die Mitglieder dieser Gruppe haben keine Beiträge verfasst.",
"details.latest_posts": "Neueste Beiträge",
"details.private": "Privat",

@ -20,7 +20,7 @@
"user_posted_topic": "<strong>%1</strong> hat ein neues Thema erstellt: <strong>%2</strong>",
"user_mentioned_you_in": "<strong>%1</strong> erwähnte dich in <strong>%2</strong>",
"user_started_following_you": "<strong>%1</strong> folgt dir jetzt.",
"new_register": "<strong>%1</strong> sent a registration request.",
"new_register": "<strong>%1</strong> hat eine Registrationsanfrage geschickt.",
"email-confirmed": "E-Mail bestätigt",
"email-confirmed-message": "Vielen Dank für Ihre E-Mail-Validierung. Ihr Konto ist nun vollständig aktiviert.",
"email-confirm-error-message": "Es gab ein Problem bei der Validierung Ihrer E-Mail-Adresse. Möglicherweise ist der Code ungültig oder abgelaufen.",

@ -15,5 +15,5 @@
"alternative_registration": "Alternative Registrierung",
"terms_of_use": "Nutzungsbedingungen",
"agree_to_terms_of_use": "Ich stimme den Nutzungsbedingungen zu",
"registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator."
"registration-added-to-queue": "Deine Registration wurde abgeschickt. Du wirst eine E-Mail erhalten, sobald sie von einem Administrator akzeptiert wird."
}

@ -5,6 +5,6 @@
"mark_as_read": "Als gelesen markieren",
"selected": "Ausgewählte",
"all": "Alle",
"all_categories": "All categories",
"all_categories": "Alle Kategorien",
"topics_marked_as_read.success": "Themen als gelesen markiert!"
}

@ -6,12 +6,12 @@
"postcount": "Beiträge",
"email": "E-Mail",
"confirm_email": "E-Mail bestätigen",
"ban_account": "Ban Account",
"ban_account_confirm": "Do you really want to ban this user?",
"unban_account": "Unban Account",
"ban_account": "Konto sperren",
"ban_account_confirm": "Sind Sie sicher, dass Sie diesen Benutzer sperren möchten?",
"unban_account": "Konto entsperren",
"delete_account": "Konto löschen",
"delete_account_confirm": "Bist du sicher, dass du dein Konto löschen möchtest? <br /><strong>Diese Aktion kann nicht rückgängig gemacht werden und du kannst deine Daten nicht wiederherstellen</strong><br /><br />Gebe deinen Benutzernamen ein, um zu bestätigen, dass du dieses Konto löschen möchtest.",
"delete_this_account_confirm": "Are you sure you want to delete this account? <br /><strong>This action is irreversible and you will not be able to recover any data</strong><br /><br />",
"delete_this_account_confirm": "Bist du sicher, dass du dieses Konto löschen möchtest?<br /><strong>Diese Aktion kann nicht rückgangig gemacht werden und du kannst die Daten nicht wiederherstellen</strong><br /><br />",
"fullname": "Kompletter Name",
"website": "Homepage",
"location": "Wohnort",
@ -68,9 +68,9 @@
"settings-require-reload": "Manche Einstellungsänderung benötigt ein aktualisieren. Drücke hier um die Seite neu zu laden.",
"has_no_follower": "Dieser User hat noch keine Follower.",
"follows_no_one": "Dieser User folgt noch niemandem :(",
"has_no_posts": "This user hasn't posted anything yet.",
"has_no_topics": "This user hasn't posted any topics yet.",
"has_no_watched_topics": "This user hasn't watched any topics yet.",
"has_no_posts": "Dieser Nutzer hat noch nichts gepostet.",
"has_no_topics": "Dieser Nutzer hat noch keine Themen gepostet.",
"has_no_watched_topics": "Dieser Nutzer beobachtet keine Themen.",
"email_hidden": "E-Mail Adresse versteckt",
"hidden": "versteckt",
"paginate_description": "Themen und Beiträge in Seiten aufteilen, anstelle unendlich zu scrollen",

@ -9,13 +9,13 @@
"filter-by": "Filtern nach",
"online-only": "Nur Online",
"picture-only": "Nur mit Bildern",
"invite": "Invite",
"invitation-email-sent": "An invitation email has been sent to %1",
"user_list": "User List",
"recent_topics": "Recent Topics",
"popular_topics": "Popular Topics",
"unread_topics": "Unread Topics",
"categories": "Categories",
"tags": "Tags",
"map": "Map"
"invite": "Einladen",
"invitation-email-sent": "Eine Einladungsemail wurde an %1 verschickt",
"user_list": "Nutzerliste",
"recent_topics": "Neueste Themen",
"popular_topics": "Beliebte Themen",
"unread_topics": "Ungelesen Themen",
"categories": "Kategorien",
"tags": "Stichwörter",
"map": "Karte"
}

@ -1,15 +1,15 @@
{
"password-reset-requested": "درخواست بازیابی گذرواژه- %1!",
"welcome-to": "به 1% خوش آمدید",
"invite": "Invitation from %1",
"invite": "دعوتنامه از %1",
"greeting_no_name": "سلام",
"greeting_with_name": "سلام 1%",
"welcome.text1": "متشکر بابت ثبت نام در %1!",
"welcome.text2": "برای فعال کردن کامل اکانت شما، ما نیاز داریم تا اطمینان حاصل کنیم که شما مالک ایمیلی که با ان ثبت نام کردید هستید.",
"welcome.text3": "An administrator has accepted your registration application. You can login with your username/password now.",
"welcome.text3": "ِک مدیر درخواست ثبت نام شما را قبول کرده. اکنون میتوانید با نام کاربری/رمز عبور خود وارد شوید",
"welcome.cta": "برای تأیید آدرس ایمیل خود اینجا کلیک کنید",
"invitation.text1": "%1 has invited you to join %2",
"invitation.ctr": "Click here to create your account.",
"invitation.text1": "%1 شما را برای پیوستن به %2 دعوت کرده",
"invitation.ctr": "برای ساخت حسابتان اینجا را کلیک کنید",
"reset.text1": "ما یک درخواست برای بازنشانی رمزعبور شما دریافت کرده ایم، احتمالا به این دلیل که شما آن را فراموش کرده اید. اگر این مورد نیست و شما رمز خود را به یاد دارید، لطفا این ایمیل را نادیده بگیرید.",
"reset.text2": "برای ادامه بازنشانی رمز، لطفابر روی این لینک کلیک کنید:",
"reset.cta": "برای تنظیم مجدد گذرواژه‌ی خود اینجا کلیک کنید",

@ -20,7 +20,7 @@
"user_posted_topic": "<strong>%1</strong> یک جستار جدید ارسال کرده: <strong>%2</strong>",
"user_mentioned_you_in": "<strong>%1</strong> در \n<strong>%1</strong> mentioned you in <strong>%2</strong> از شما نام برده",
"user_started_following_you": "<strong>%1</strong> شروع به دنبال کردن شما کرده",
"new_register": "<strong>%1</strong> sent a registration request.",
"new_register": "<strong>%1</strong> یک درخواست ثبت نام ارسال کرده است",
"email-confirmed": "رایانامه تایید شد",
"email-confirmed-message": "بابت تایید ایمیلتان سپاس‌گزاریم. حساب کاربری شما اکنون به صورت کامل فعال شده است.",
"email-confirm-error-message": "خطایی در تایید آدرس ایمیل شما پیش آمده است. ممکن است کد نا‌معتبر و یا منقضی شده باشد.",

@ -15,5 +15,5 @@
"alternative_registration": "روش نام‌نویسی جایگزین",
"terms_of_use": "شرایط استفاده",
"agree_to_terms_of_use": "با شرایط استفاده موافقم",
"registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator."
"registration-added-to-queue": "ثبت نام شما به صف تایید اضافه شد. وقتی توسط یک مدیر تایید شد شما رایانامه ای دریافت خواهید کرد."
}

@ -5,7 +5,7 @@
"no_topics_found": "هیچ جستاری یافت نشد!",
"no_posts_found": "دیدگاهی یافت نشد!",
"post_is_deleted": "این دیدگاه پاک شده!",
"topic_is_deleted": "This topic is deleted!",
"topic_is_deleted": "جستار حذف شده است!",
"profile": "نمایه",
"posted_by": "ارسال شده توسط %1",
"posted_by_guest": "ارسال شده توسط مهمان",

@ -5,6 +5,6 @@
"mark_as_read": "خوانده شده بگیر",
"selected": "برگزیده",
"all": "همه",
"all_categories": "All categories",
"all_categories": "تمام دسته ها",
"topics_marked_as_read.success": "همه جستارها خوانده شدند"
}

@ -25,7 +25,7 @@
"watched": "پاییده شده",
"followers": "دنبال‌کننده‌ها",
"following": "دنبال‌شونده‌ها",
"aboutme": "About me",
"aboutme": "درباره ی من",
"signature": "امضا",
"gravatar": "گراواتار",
"birthday": "روز تولد",
@ -68,21 +68,21 @@
"settings-require-reload": "تغییر برخی تنظیمات مستلزم بارگذاری مجدد هستند. برای بارگذاری مجدد صفحه اینجا کلیک کنید.",
"has_no_follower": "این کاربر هیچ دنبال‌کننده‌ای ندارد :(",
"follows_no_one": "این کاربر هیچ کسی را دنبال نمی‌کند :(",
"has_no_posts": "This user hasn't posted anything yet.",
"has_no_topics": "This user hasn't posted any topics yet.",
"has_no_watched_topics": "This user hasn't watched any topics yet.",
"has_no_posts": "این کاربر تا به حال هیچ چیزی ارسال نکرده است.",
"has_no_topics": "این کاربر تا به حال هیچ جستاری ارسال نکرده است",
"has_no_watched_topics": "این کاربر تا به حال هیچ جستاری را نپاییده است",
"email_hidden": "رایانامه پنهان شده",
"hidden": "پنهان",
"paginate_description": "Paginate topics and posts instead of using infinite scroll",
"topics_per_page": "شمار جستارها در هر برگه",
"posts_per_page": "شمار دیدگاه‌ها در هر برگه",
"notification_sounds": "Play a sound when you receive a notification",
"notification_sounds": "پخش صدا زمانی که یک آگاه سازی دریافت میکنید",
"browsing": "تنظیمات مرور",
"open_links_in_new_tab": "Open outgoing links in new tab",
"open_links_in_new_tab": "پیوندهای به بیرون را در برگ جدید باز کن",
"enable_topic_searching": "فعال کردن جستجوی داخل-جستار ",
"topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen",
"follow_topics_you_reply_to": "Follow topics that you reply to",
"follow_topics_you_create": "Follow topics you create",
"follow_topics_you_reply_to": "تاپیک هایی که پاسخ داده ای را دنبال کن",
"follow_topics_you_create": "جستارهایی که ایجاد کرده ای را دنبال کن",
"grouptitle": "عنوان گروهی که میخواهید نشان داده شود را انتخاب کنید.",
"no-group-title": "عنوان گروه ای نیست"
}

@ -9,13 +9,13 @@
"filter-by": "غربال با",
"online-only": "فقط آنلاین",
"picture-only": "عکس فقط",
"invite": "Invite",
"invitation-email-sent": "An invitation email has been sent to %1",
"user_list": "User List",
"recent_topics": "Recent Topics",
"popular_topics": "Popular Topics",
"unread_topics": "Unread Topics",
"categories": "Categories",
"tags": "Tags",
"map": "Map"
"invite": "دعوت",
"invitation-email-sent": "رایانامه ی دعوتنامه به %1 ارسال شد",
"user_list": "فهرست کاربران",
"recent_topics": "جستارهای اخیر",
"popular_topics": "جستارهای محبوب",
"unread_topics": "جستارهای خوانده نشده",
"categories": "دسته ها",
"tags": "برچسب‌ها",
"map": "نقشه"
}

@ -1,15 +1,15 @@
{
"password-reset-requested": "Demande de réinitialisation du mot de passe - %1!",
"welcome-to": "Bienvenue sur %1",
"invite": "Invitation from %1",
"invite": "Invitation de %1",
"greeting_no_name": "Bonjour",
"greeting_with_name": "Bonjour %1",
"welcome.text1": "Merci de vous être inscrit sur %1!",
"welcome.text2": "Pour activer totalement votre compte, nous devons vérifier que vous êtes bien propriétaire de l'adresse email que vous avez utilisé pour vous inscrire.",
"welcome.text3": "An administrator has accepted your registration application. You can login with your username/password now.",
"welcome.text3": "Un administrateur a accepté votre demande d'inscription. Vous pouvez maintenant vous connecter avec vos identifiants/mots de passe.",
"welcome.cta": "Cliquez ici pour confirmer votre adresse email",
"invitation.text1": "%1 has invited you to join %2",
"invitation.ctr": "Click here to create your account.",
"invitation.text1": "%1 vous a invité à joindre %2",
"invitation.ctr": "Cliquer ici pour créer votre compte.",
"reset.text1": "Nous avons reçu une demande de réinitialisation de votre mot de passe, probablement parce que vous l'avez oublié. Si ce n'est pas le cas, veuillez ignorer cet email.",
"reset.text2": "Pour confirmer la réinitialisation de votre mot de passe, veuillez cliquer sur le lien suivant :",
"reset.cta": "Cliquez ici pour réinitialiser votre mot de passe",

@ -51,7 +51,7 @@
"already-favourited": "Vous avez déjà mis ce message en favoris",
"already-unfavourited": "Vous avez déjà retiré ce message des favoris",
"cant-ban-other-admins": "Vous ne pouvez pas bannir les autres administrateurs !",
"cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin",
"cant-remove-last-admin": "Vous seul êtes administrateur. Ajouter un autre utilisateur en tant qu'administrateur avant de vous en retirer.",
"invalid-image-type": "Type d'image invalide. Les types autorisés sont: %1",
"invalid-image-extension": "Extension d'image invalide",
"invalid-file-type": "Type de fichier non valide. Les types autorisés sont : %1",
@ -60,8 +60,8 @@
"group-name-change-not-allowed": "Modification du nom de groupe non permise",
"group-already-member": "Vous faites déjà parti de ce groupe",
"group-needs-owner": "Ce groupe nécessite au moins un propriétaire",
"group-already-invited": "This user has already been invited",
"group-already-requested": "Your membership request has already been submitted",
"group-already-invited": "Cet utilisateur a déjà été invité.",
"group-already-requested": "Votre demande d'adhésion a déjà été envoyée.",
"post-already-deleted": "Message déjà supprimé",
"post-already-restored": "Message déjà restauré",
"topic-already-deleted": "Sujet déjà supprimé",
@ -79,7 +79,7 @@
"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",
"already-flagged": "You have already flagged this post",
"already-flagged": "Vous avez déjà signalé ce 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 .",
"registration-error": "Erreur d'enregistrement",
"parse-error": "Une erreur est survenue en analysant la réponse du serveur",

@ -50,7 +50,7 @@
"views": "Vues",
"reputation": "Réputation",
"read_more": "En lire plus",
"more": "More",
"more": "Plus",
"posted_ago_by_guest": "posté %1 par un invité",
"posted_ago_by": "posté %1 par %2",
"posted_ago": "posté %1",

@ -6,12 +6,12 @@
"no_groups_found": "Il n'y a aucun groupe",
"pending.accept": "Accepter",
"pending.reject": "Refuser",
"pending.accept_all": "Accept All",
"pending.reject_all": "Reject All",
"pending.none": "There are no pending members at this time",
"invited.none": "There are no invited members at this time",
"invited.uninvite": "Rescind Invitation",
"invited.search": "Search for a user to invite to this group",
"pending.accept_all": "Tout accepter",
"pending.reject_all": "Tout rejeter",
"pending.none": "Il n'y a aucun membre en attente pour le moment",
"invited.none": "Il n'y a aucun membre invité pour le moment",
"invited.uninvite": "Résilier l'invitation",
"invited.search": "Chercher un utilisateur a inviter dans ce groupe",
"cover-instructions": "Glissez-déposez une image, ajustez la position, et cliquez sur <strong>Enregistrer</strong>",
"cover-change": "Modifier",
"cover-save": "Enregistrer",
@ -19,7 +19,7 @@
"details.title": "Informations du groupe",
"details.members": "Liste des membres",
"details.pending": "Membres en attente",
"details.invited": "Invited Members",
"details.invited": "Inviter des Membres",
"details.has_no_posts": "Les membres de ce groupe n'ont envoyé aucun message.",
"details.latest_posts": "Derniers messages",
"details.private": "Privé",

@ -20,7 +20,7 @@
"user_posted_topic": "<strong>%1</strong> a posté un nouveau sujet: <strong>%2</strong>.",
"user_mentioned_you_in": "<strong>%1</strong> vous a mentionné dans <strong>%2</strong>",
"user_started_following_you": "<strong>%1</strong> vous suit.",
"new_register": "<strong>%1</strong> sent a registration request.",
"new_register": "<strong>%1</strong> a envoyé une demande d'incription.",
"email-confirmed": "Email vérifié",
"email-confirmed-message": "Merci pour la validation de votre adresse email. Votre compte est désormais activé.",
"email-confirm-error-message": "Il y a un un problème dans la vérification de votre adresse email. Le code est peut être invalide ou a expiré.",

@ -15,5 +15,5 @@
"alternative_registration": "Autres méthodes d'inscription",
"terms_of_use": "Conditions d'utilisation",
"agree_to_terms_of_use": "J'accepte les Conditions d'utilisation",
"registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator."
"registration-added-to-queue": "Votre inscription a été ajoutée à la liste d'approbation. Vous recevrez un email quand celle-ci sera acceptée par un administrateur."
}

@ -5,6 +5,6 @@
"mark_as_read": "Marquer comme lu",
"selected": "Sélectionnés",
"all": "Tous",
"all_categories": "All categories",
"all_categories": "Toutes Catégories",
"topics_marked_as_read.success": "Sujets marqués comme lus !"
}

@ -6,12 +6,12 @@
"postcount": "Nombre de messages",
"email": "Email",
"confirm_email": "Confirmer l'adresse email",
"ban_account": "Bannir un compte",
"ban_account": "Bannir",
"ban_account_confirm": "Êtes-vous sûr de bien vouloir bannir cet utilisateur ?",
"unban_account": "Unban Account",
"unban_account": "Restaurer le Compte",
"delete_account": "Supprimer le compte",
"delete_account_confirm": "Êtes-vous sûr de vouloir supprimer votre compte? <br /> <strong> Cette action est irréversible et vous ne serez pas en mesure de récupérer vos données</ strong> <br /> <br /> Entrez votre nom d'utilisateur pour confirmer que vous souhaitez détruire votre compte.",
"delete_this_account_confirm": "Are you sure you want to delete this account? <br /><strong>This action is irreversible and you will not be able to recover any data</strong><br /><br />",
"delete_this_account_confirm": "Etes-vous sûr de vouloir supprimer ce compte? <br /><strong>Cette action est irréversible et vous ne pourrez récupérer aucune donnée.",
"fullname": "Nom",
"website": "Site web",
"location": "Emplacement",
@ -68,9 +68,9 @@
"settings-require-reload": "Certains réglages nécessitent un rechargement. Cliquez ici pour recharger la page.",
"has_no_follower": "Cet utilisateur n'est suivi par personne :(",
"follows_no_one": "Cet utilisateur ne suit personne :(",
"has_no_posts": "This user hasn't posted anything yet.",
"has_no_topics": "This user hasn't posted any topics yet.",
"has_no_watched_topics": "This user hasn't watched any topics yet.",
"has_no_posts": "Cet utilisateur n'a encore rien posté.",
"has_no_topics": "Cet utilisateur n'a encore créé aucun sujet.",
"has_no_watched_topics": "Cet utilisateur n'a encore consulté aucun sujet.",
"email_hidden": "Email masqué",
"hidden": "masqué",
"paginate_description": "Utiliser la pagination des sujets et des messages à la place du défilement infini.",

@ -9,13 +9,13 @@
"filter-by": "Filtrer par",
"online-only": "En ligne uniquement",
"picture-only": "Avec image uniquement",
"invite": "Invite",
"invitation-email-sent": "An invitation email has been sent to %1",
"user_list": "User List",
"recent_topics": "Recent Topics",
"popular_topics": "Popular Topics",
"unread_topics": "Unread Topics",
"categories": "Categories",
"tags": "Tags",
"map": "Map"
"invite": "Invitation",
"invitation-email-sent": "Un email d'invitation a été envoyé à %1",
"user_list": "Liste d'Utilisateurs",
"recent_topics": "Sujets Récents",
"popular_topics": "Sujets Populaires",
"unread_topics": "Sujets Non-Lus",
"categories": "Catégories",
"tags": "Mots-clés",
"map": "Carte"
}

@ -20,7 +20,7 @@
"user_posted_topic": "<strong>%1</strong> ha postato un nuovo Topic: <strong>%2</strong>",
"user_mentioned_you_in": "<strong>%1</strong> ti ha menzionato in <strong>%2</strong>",
"user_started_following_you": "<strong>%1</strong> ha iniziato a seguirti.",
"new_register": "<strong>%1</strong> sent a registration request.",
"new_register": "<strong>%1</strong> ha inviato una richiesta di registrazione.",
"email-confirmed": "Email Confermata",
"email-confirmed-message": "Grazie per aver validato la tua email. Il tuo account è ora completamente attivato.",
"email-confirm-error-message": "C'è stato un problema nella validazione del tuo indirizzo email. Potrebbe essere il codice non valido o scaduto.",

@ -2,7 +2,7 @@
"new_topic_button": "Nieuw onderwerp",
"guest-login-post": "Log in om een reactie te plaatsen",
"no_topics": "<strong>Er zijn geen onderwerpen in deze categorie.</strong><br />Waarom maak je er niet een aan?",
"browsing": "verkennen",
"browsing": "browsing",
"no_replies": "Niemand heeft gereageerd",
"share_this_category": "Deel deze categorie",
"watch": "Volgen",

@ -1,15 +1,15 @@
{
"password-reset-requested": "Om wachtwoordherstel verzocht - %1!",
"password-reset-requested": "Wachtwoord reset gevraagd - %1!",
"welcome-to": "Welkom bij %1",
"invite": "Invitation from %1",
"invite": "Uitnodiging van %1 ",
"greeting_no_name": "Hallo",
"greeting_with_name": "Hallo %1",
"welcome.text1": "Bedank voor het registreren bij %1!",
"welcome.text2": "Om de account volledig te activeren, dienen de instructies uit het bevestigingsbericht opgevolgd te worden. Controleer daarom nu eerst de e-mail inbox voor de activeringscode en volg de link in het bericht.",
"welcome.text3": "An administrator has accepted your registration application. You can login with your username/password now.",
"welcome.text3": "Een administrator heeft uw registratie geaccepteerd. U kan nu inloggen met uw gebruikersnaam en wachtwoord.",
"welcome.cta": "Klik hier voor bevestigen van het e-mailadres",
"invitation.text1": "%1 has invited you to join %2",
"invitation.ctr": "Click here to create your account.",
"invitation.text1": "%1 heeft u uitgenodigd voor %2 ",
"invitation.ctr": "Klik hier om uw account aan te maken.",
"reset.text1": "Wij ontvingen zojuist een verzoek om het wachtwoord van de account, bij onze website bekend als gekoppeld aan dit e-mailadres, te herstellen. Is dit verzoek niet bekend en geautoriseerd, dan kan dit bericht genegeerd worden.",
"reset.text2": "Om het wachtwoord opnieuw in te stellen, klik op deze link:",
"reset.cta": "Klik hier voor wachtwoordherstel",

@ -51,7 +51,7 @@
"already-favourited": "Dit bericht staat al tussen de favorieten",
"already-unfavourited": "Dit bericht is al uit favorieten verwijderd",
"cant-ban-other-admins": "Het is niet toegestaan andere beheerders te verbannen!",
"cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin",
"cant-remove-last-admin": "U bent de enigen administrator. Voeg een andere gebruiker toe als administrator voordat u uw zelf verweiderd als admin",
"invalid-image-type": "Ongeldig bestandstype afbeelding. Deze afbeelding is van een bestandstype dat niet ondersteund wordt. Toegestane bestandstypes voor afbeeldingsbestanden zijn: %1",
"invalid-image-extension": "Ongeldige bestandstype afbeelding",
"invalid-file-type": "Dit bestandstype wordt niet ondersteund. Toegestane bestandstypen zijn: %1",
@ -60,8 +60,8 @@
"group-name-change-not-allowed": "Het veranderen van de groepsnaam is niet toegestaan!",
"group-already-member": "Groepslidmaatschap al aanwezig",
"group-needs-owner": "De groep vereist ten minste 1 eigenaar",
"group-already-invited": "This user has already been invited",
"group-already-requested": "Your membership request has already been submitted",
"group-already-invited": "Deze gebruiker is all uitgenodigt ",
"group-already-requested": "Uw lidmaatschap aanvraag is all verstuurd",
"post-already-deleted": "Dit bericht is al verwijderd",
"post-already-restored": "Dit bericht is al hersteld",
"topic-already-deleted": "Dit onderwerp is al verwijderd",
@ -79,7 +79,7 @@
"downvoting-disabled": "Negatief stemmen staat uitgeschakeld.",
"not-enough-reputation-to-downvote": "Deze gebruikersaccount beschikt over onvoldoende reputatie om een negatieve stem uit te mogen brengen.",
"not-enough-reputation-to-flag": "Onvoldoende reputatie om dit bericht aan beheerders te mogen melden.",
"already-flagged": "You have already flagged this post",
"already-flagged": "U heeft deze post all gerapporteerd ",
"reload-failed": "Tijdens het herladen van \"%1\" is NodeBB een fout of probleem tegen gekomen. NodeBB blijft operationeel echter het is verstandig om de oorzaak te onderzoeken en wellicht de vorige actie, voor het herladen, ongedaan te maken.",
"registration-error": "Fout tijdens registratie",
"parse-error": "Tijdens het verwerken van het antwoord van de server is iets misgegaan.",

@ -50,7 +50,7 @@
"views": "Gezien",
"reputation": "Reputatie",
"read_more": "Lees meer",
"more": "More",
"more": "Meer",
"posted_ago_by_guest": "geplaatst %1 door gast",
"posted_ago_by": "geplaatst %1 door %2",
"posted_ago": "geplaatst door %1",

@ -6,12 +6,12 @@
"no_groups_found": "Geen groepen voor weergave",
"pending.accept": "Accepteer",
"pending.reject": "Afwijzen",
"pending.accept_all": "Accept All",
"pending.reject_all": "Reject All",
"pending.none": "There are no pending members at this time",
"invited.none": "There are no invited members at this time",
"invited.uninvite": "Rescind Invitation",
"invited.search": "Search for a user to invite to this group",
"pending.accept_all": "Iedereen accepteren",
"pending.reject_all": "Iedereen afwijzen",
"pending.none": "Er zijn geen afwachtende leden op het moment",
"invited.none": "Er zijn geen uitgenodigde leden op het moment",
"invited.uninvite": "Uitnodiging intrekken",
"invited.search": "Zoek naar een gebruiker om uit te nodigen voor deze groep",
"cover-instructions": "Sleep een afbeelding, sleep om te positioneren en klik tenslotte op <strong>Opslaan</strong>",
"cover-change": "Bewerken",
"cover-save": "Opslaan",
@ -19,22 +19,22 @@
"details.title": "Groepsdetails",
"details.members": "Ledenlijst",
"details.pending": "Nog niet geaccepteerde leden",
"details.invited": "Invited Members",
"details.invited": "Uitgenodigde leden",
"details.has_no_posts": "Deze groepleden hebben nog geen berichten geplaatst",
"details.latest_posts": "Meest recente berichten",
"details.private": "Prive",
"details.grant": "Toekennen/herroepen van eigendom",
"details.kick": "Schoppen",
"details.kick": "Kick",
"details.owner_options": "Groepsadministratie",
"details.group_name": "Groepsnaam",
"details.member_count": "Ledentelling",
"details.creation_date": "Aangemaakt op",
"details.description": "Beschrijving",
"details.badge_preview": "Draaginsigne voorvertoning",
"details.badge_preview": "Badge Voorbeeld",
"details.change_icon": "Wijzig icoon",
"details.change_colour": "Wijzig kleur",
"details.badge_text": "Draaginsigne tekst",
"details.userTitleEnabled": "Draaginsignes weergeven",
"details.badge_text": "Badge Tekst",
"details.userTitleEnabled": "Badge Weergeven",
"details.private_help": "Wanneer ingeschakeld, zal eerst een groepseigenaar goedkeuring moeten verlenen voordat nieuwe leden kunnen toetreden",
"details.hidden": "Niet getoond",
"details.hidden_help": "Indien geactiveerd zal deze groep niet getoond worden in de groepslijst en zullen gebruikers handmatig uitgenodigd moeten worden.",

@ -20,7 +20,7 @@
"user_posted_topic": "<strong>%1</strong> heeft een nieuw onderwerp geplaatst: <strong>%2</strong>",
"user_mentioned_you_in": "Onze naam is genoemd door <strong>%1</strong> in <strong>%2</strong>.",
"user_started_following_you": "<strong>%1</strong> volgt ons nu.",
"new_register": "<strong>%1</strong> sent a registration request.",
"new_register": "<strong>%1</strong> heeft een registratie verzoek aangevraagd.",
"email-confirmed": "E-mailadres bevestigd",
"email-confirmed-message": "Bedankt voor het bevestigen van het e-mailadres. Deze account is nu volledig geactiveerd.",
"email-confirm-error-message": "Er was een probleem met het bevestigen van dit e-mailadres. Misschien is de code niet goed ingevoerd of was de beschikbare tijd inmiddels verstreken.",

@ -4,7 +4,7 @@
"week": "Week",
"month": "Maand",
"year": "Jaar",
"alltime": "Intussen",
"alltime": "altijd",
"no_recent_topics": "Er zijn geen recente reacties.",
"no_popular_topics": "Er zijn geen populaire onderwerpen.",
"there-is-a-new-topic": "Er is een nieuw onderwerp",

@ -15,5 +15,5 @@
"alternative_registration": "Alternatieve Registratie",
"terms_of_use": "Gebruiksvoorwaarden",
"agree_to_terms_of_use": "Ik ga akkoord van de Gebruiksvoorwaarden",
"registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator."
"registration-added-to-queue": "Uw registratie is toegevoegd aan de wachtrij. U krijgt een email wanneer uw registratie is geaccepteerd "
}

@ -9,8 +9,8 @@
"in-categories": "In categorieën",
"search-child-categories": "Doorzoek subcategorieën ",
"reply-count": "Aantal reacties",
"at-least": "Minimaal",
"at-most": "Maximaal",
"at-least": "op zijn minst",
"at-most": "op zijn meest",
"post-time": "Geplaatst op",
"newer-than": "Nieuwer dan",
"older-than": "Ouder dan",

@ -27,15 +27,15 @@
"locked": "Gesloten",
"bookmark_instructions": "Klik hier om naar de vorige positie terug te keren of sluit af om te verwerpen.",
"flag_title": "Bericht aan beheerders melden",
"flag_confirm": "Is het echt de bedoeling dit bericht aan beheerders te rapporteren?",
"flag_confirm": "Weet u het zeker dat u dit bericht wilt rapporteren?",
"flag_success": "Het bericht is gerapporteerd aan beheer.",
"deleted_message": "Dit onderwerp is verwijderd. Alleen gebruikers met beheerrechten op onderwerpniveau kunnen dit inzien.",
"following_topic.message": "Vanaf nu worden meldingen ontvangen zodra iemand een reactie op dit onderwerp geeft.",
"not_following_topic.message": "Er worden niet langer meldingen ontvangen over dit onderwerp.",
"login_to_subscribe": "Aanmelden om op dit onderwerp te abonneren",
"not_following_topic.message": "U ontvangt geen notificaties over dit onderwerp.",
"login_to_subscribe": "Log in or registreer om dit onderwerp te volgen.",
"markAsUnreadForAll.success": "Onderwerp is voor iedereen als 'gelezen' gemarkeerd.",
"watch": "Volgen",
"unwatch": "Niet volgen",
"unwatch": "Unfollow",
"watch.title": "Krijg meldingen van nieuwe reacties op dit onderwerp",
"unwatch.title": "Dit onderwerp niet langer volgen",
"share_this_post": "Deel dit bericht",
@ -49,8 +49,8 @@
"thread_tools.move_all": "Verplaats alles",
"thread_tools.fork": "Onderwerp afsplitsen",
"thread_tools.delete": "Onderwerp verwijderen",
"thread_tools.delete_confirm": "Is het echt de bedoeling dit onderwerp te verwijderen?",
"thread_tools.restore": "Onderwerp erstellen",
"thread_tools.delete_confirm": "Weet u het zeker dat u dit onderwerp wilt verwijderen?",
"thread_tools.restore": "Onderwerp herstellen",
"thread_tools.restore_confirm": "Zeker weten dit onderwerp te herstellen?",
"thread_tools.purge": "Wis onderwerp ",
"thread_tools.purge_confirm": "Is het echt de bedoeling dit onderwerp definitief te wissen?",
@ -59,7 +59,7 @@
"post_restore_confirm": "Is het de bedoeling dit bericht te herstellen?",
"post_purge_confirm": "Is het absoluut zeker dat dit bericht volledig verwijderd kan worden?",
"load_categories": "Categorieën laden",
"disabled_categories_note": "Uitgeschakelde categorieën zijn grijs",
"disabled_categories_note": "Uitgeschakelde Categorieën zijn grijs",
"confirm_move": "Verplaatsen",
"confirm_fork": "Splits",
"favourite": "Favoriet",
@ -86,7 +86,7 @@
"composer.thumb_title": "Voeg een miniatuurweergave toe aan dit onderwerp",
"composer.thumb_url_placeholder": "http://example.com/thumb.png",
"composer.thumb_file_label": "Of upload een bestand",
"composer.thumb_remove": "Velden legen",
"composer.thumb_remove": "Velden leegmaken",
"composer.drag_and_drop_images": "Sleep en zet afbeeldingen hier",
"more_users_and_guests": "%1 of meerdere gebruiker(s) en %2 gast(en)",
"more_users": "%1 meer gebruiker(s)",

@ -5,6 +5,6 @@
"mark_as_read": "Markeer als gelezen",
"selected": "Geselecteerd",
"all": "Alles",
"all_categories": "All categories",
"all_categories": "Alle categorieën",
"topics_marked_as_read.success": "Onderwerp gemarkeerd als gelezen!"
}

@ -6,12 +6,12 @@
"postcount": "Aantal geplaatste berichten",
"email": "E-mail",
"confirm_email": "Bevestig e-mail",
"ban_account": "Ban Account",
"ban_account_confirm": "Do you really want to ban this user?",
"ban_account": "Verban Account",
"ban_account_confirm": "Weet u zeker dat u deze gebruiker wilt verbannen",
"unban_account": "Unban Account",
"delete_account": "Account verwijderen",
"delete_account_confirm": "Controleer of dat het zeker is dat deze account verwijderd moet worden. <br /><strong> Deze actie kan niet ongedaan gemaakt worden en herstellen van gebruiker- of profielgegevens is niet mogelijk</strong><br /><br /> Typ hier de gebruikersnaam als extra controle om te bevestigen dat deze account verwijderd moet worden.",
"delete_this_account_confirm": "Are you sure you want to delete this account? <br /><strong>This action is irreversible and you will not be able to recover any data</strong><br /><br />",
"delete_this_account_confirm": "Weet u zeker dat u deze account wilt verwijderen? <br /><strong>Deze actie kan niet ongedaan worden en u kunt niet de informatie herstellen</strong><br /><br />",
"fullname": "Volledige naam",
"website": "Website",
"location": "Locatie",
@ -68,9 +68,9 @@
"settings-require-reload": "Sommige veranderingen vereisen het herladen van de pagina: klik hier om de pagina te herladen.",
"has_no_follower": "Deze gebruiker heeft geen volgers :(",
"follows_no_one": "Deze gebruiker volgt niemand :(",
"has_no_posts": "This user hasn't posted anything yet.",
"has_no_topics": "This user hasn't posted any topics yet.",
"has_no_watched_topics": "This user hasn't watched any topics yet.",
"has_no_posts": "Deze gebruiker heeft nog geen berichten geplaatst",
"has_no_topics": "Deze gebruiker heeft nog geen onderwerpen gestart.",
"has_no_watched_topics": "Deze gebruiker heeft nog geen onderwerpen gevolgd.",
"email_hidden": "E-mail niet beschikbaar",
"hidden": "verborgen",
"paginate_description": "Blader door onderwerpen en berichten in plaats van oneindig scrollen.",

@ -9,13 +9,13 @@
"filter-by": "Filter op",
"online-only": "Online ",
"picture-only": "Alleen een afbeelding",
"invite": "Invite",
"invitation-email-sent": "An invitation email has been sent to %1",
"user_list": "User List",
"recent_topics": "Recent Topics",
"popular_topics": "Popular Topics",
"unread_topics": "Unread Topics",
"categories": "Categories",
"invite": "Uitnodigen",
"invitation-email-sent": "Een uitnodiging email is verstuurd naar %1 ",
"user_list": "Ledenlijst",
"recent_topics": "Recente onderwerpen",
"popular_topics": "Populaire onderwerpen",
"unread_topics": "Ongelezen onderwerpen",
"categories": "Categorieën",
"tags": "Tags",
"map": "Map"
}

@ -5,8 +5,8 @@
"browsing": "正在浏览",
"no_replies": "尚无回复",
"share_this_category": "分享此版块",
"watch": "订阅",
"watch": "关注",
"ignore": "忽略",
"watch.message": "您现在已经订阅了此版块",
"ignore.message": "您现在已经取消订阅了此版块"
"watch.message": "您现在已经关注了此版块",
"ignore.message": "您现在已经取消了此版块的关注"
}

@ -9,13 +9,13 @@
"filter-by": "过滤选项",
"online-only": "只看在线",
"picture-only": "只看图片",
"invite": "Invite",
"invitation-email-sent": "An invitation email has been sent to %1",
"user_list": "User List",
"recent_topics": "Recent Topics",
"popular_topics": "Popular Topics",
"unread_topics": "Unread Topics",
"categories": "Categories",
"tags": "Tags",
"map": "Map"
"invite": "邀请注册",
"invitation-email-sent": "已发送邀请给 %1",
"user_list": "会员列表",
"recent_topics": "最新主题",
"popular_topics": "热门主题",
"unread_topics": "未读主题",
"categories": "版面",
"tags": "话题",
"map": "地图"
}

@ -1,312 +1,312 @@
"use strict";
var ajaxify = ajaxify || {};
$(document).ready(function() {
/*global app, templates, utils, socket, config, RELATIVE_PATH*/
var location = document.location || window.location,
rootUrl = location.protocol + '//' + (location.hostname || location.host) + (location.port ? ':' + location.port : ''),
apiXHR = null,
translator;
// Dumb hack to fool ajaxify into thinking translator is still a global
// When ajaxify is migrated to a require.js module, then this can be merged into the "define" call
require(['translator'], function(_translator) {
translator = _translator;
});
$(window).on('popstate', function (ev) {
ev = ev.originalEvent;
if (ev !== null && ev.state && ev.state.url !== undefined) {
ajaxify.go(ev.state.url, function() {
$(window).trigger('action:popstate', {url: ev.state.url});
}, true);
}
});
ajaxify.currentPage = null;
ajaxify.go = function (url, callback, quiet, search) {
if (!socket.connected) {
if (ajaxify.reconnectAction) {
$(window).off('action:reconnected', ajaxify.reconnectAction);
}
ajaxify.reconnectAction = function(e) {
ajaxify.go(url, callback, quiet, search);
$(window).off(e);
}
$(window).on('action:reconnected', ajaxify.reconnectAction);
}
if (ajaxify.handleRedirects(url)) {
return true;
}
app.enterRoom('');
$(window).off('scroll');
if ($('#content').hasClass('ajaxifying') && apiXHR) {
apiXHR.abort();
}
url = ajaxify.start(url, quiet, search);
$('#footer, #content').removeClass('hide').addClass('ajaxifying');
ajaxify.variables.flush();
ajaxify.loadData(url, function(err, data) {
if (err) {
return onAjaxError(err, url, callback, quiet);
}
app.template = data.template.name;
require(['translator'], function(translator) {
translator.load(config.defaultLang, data.template.name);
renderTemplate(url, data.template.name, data, callback);
});
});
return true;
};
ajaxify.handleRedirects = function(url) {
url = ajaxify.removeRelativePath(url.replace(/\/$/, '')).toLowerCase();
var isAdminRoute = url.startsWith('admin') && window.location.pathname.indexOf(RELATIVE_PATH + '/admin') !== 0;
var uploadsOrApi = url.startsWith('uploads') || url.startsWith('api');
if (isAdminRoute || uploadsOrApi) {
window.open(RELATIVE_PATH + '/' + url, '_top');
return true;
}
return false;
};
ajaxify.start = function(url, quiet, search) {
url = ajaxify.removeRelativePath(url.replace(/^\/|\/$/g, ''));
var hash = window.location.hash;
search = search || '';
$(window).trigger('action:ajaxify.start', {url: url});
if (!window.location.pathname.match(/\/(403|404)$/g)) {
app.previousUrl = window.location.href;
}
ajaxify.currentPage = url;
if (window.history && window.history.pushState) {
window.history[!quiet ? 'pushState' : 'replaceState']({
url: url + search + hash
}, url, RELATIVE_PATH + '/' + url + search + hash);
}
return url;
};
function onAjaxError(err, url, callback, quiet) {
var data = err.data,
textStatus = err.textStatus;
if (data) {
var status = parseInt(data.status, 10);
if (status === 403 || status === 404 || status === 500 || status === 502) {
if (status === 502) {
status = 500;
}
$('#footer, #content').removeClass('hide').addClass('ajaxifying');
return renderTemplate(url, status.toString(), data.responseJSON, (new Date()).getTime(), callback);
} else if (status === 401) {
app.alertError('[[global:please_log_in]]');
app.previousUrl = url;
return ajaxify.go('login');
} else if (status === 302) {
if (data.responseJSON.external) {
window.location.href = data.responseJSON.external;
} else if (typeof data.responseJSON === 'string') {
ajaxify.go(data.responseJSON.slice(1), callback, quiet);
}
}
} else if (textStatus !== 'abort') {
app.alertError(data.responseJSON.error);
}
}
function renderTemplate(url, tpl_url, data, callback) {
$(window).trigger('action:ajaxify.loadingTemplates', {});
templates.parse(tpl_url, data, function(template) {
translator.translate(template, function(translatedTemplate) {
$('#content').html(translatedTemplate);
ajaxify.end(url, tpl_url);
if (typeof callback === 'function') {
callback();
}
$('#content, #footer').removeClass('ajaxifying');
app.refreshTitle(url);
});
});
}
ajaxify.end = function(url, tpl_url) {
function done() {
if (--count === 0) {
$(window).trigger('action:ajaxify.end', {url: url});
}
}
var count = 2;
ajaxify.variables.parse();
ajaxify.loadScript(tpl_url, done);
ajaxify.widgets.render(tpl_url, url, done);
$(window).trigger('action:ajaxify.contentLoaded', {url: url, tpl: tpl_url});
app.processPage();
};
ajaxify.removeRelativePath = function(url) {
if (url.startsWith(RELATIVE_PATH.slice(1))) {
url = url.slice(RELATIVE_PATH.length);
}
return url;
};
ajaxify.refresh = function(e) {
if (e && e instanceof jQuery.Event) {
e.preventDefault();
}
ajaxify.go(ajaxify.currentPage, null, true);
};
ajaxify.loadScript = function(tpl_url, callback) {
var location = !app.inAdmin ? 'forum/' : '';
require([location + tpl_url], function(script) {
if (script && script.init) {
script.init();
}
if (callback) {
callback();
}
});
};
ajaxify.loadData = function(url, callback) {
url = ajaxify.removeRelativePath(url);
$(window).trigger('action:ajaxify.loadingData', {url: url});
apiXHR = $.ajax({
url: RELATIVE_PATH + '/api/' + url,
cache: false,
success: function(data) {
if (!data) {
return;
}
ajaxify.data = data;
data.relative_path = RELATIVE_PATH;
$(window).trigger('action:ajaxify.dataLoaded', {url: url, data: data});
if (callback) {
callback(null, data);
}
},
error: function(data, textStatus) {
if (data.status === 0 && textStatus === 'error') {
data.status = 500;
}
callback({
data: data,
textStatus: textStatus
});
}
});
};
ajaxify.loadTemplate = function(template, callback) {
if (templates.cache[template]) {
callback(templates.cache[template]);
} else {
$.ajax({
url: RELATIVE_PATH + '/templates/' + template + '.tpl' + (config['cache-buster'] ? '?v=' + config['cache-buster'] : ''),
type: 'GET',
success: function(data) {
callback(data.toString());
},
error: function(error) {
throw new Error("Unable to load template: " + template + " (" + error.statusText + ")");
}
});
}
};
function ajaxifyAnchors() {
templates.registerLoader(ajaxify.loadTemplate);
function hrefEmpty(href) {
return href === undefined || href === '' || href === 'javascript:;';
}
// Enhancing all anchors to ajaxify...
$(document.body).on('click', 'a', function (e) {
if (this.target !== '' || (this.protocol !== 'http:' && this.protocol !== 'https:')) {
return;
} else if (hrefEmpty(this.href) || this.protocol === 'javascript:' || $(this).attr('data-ajaxify') === 'false' || $(this).attr('href') === '#') {
return e.preventDefault();
}
if (!e.ctrlKey && !e.shiftKey && !e.metaKey && e.which === 1) {
if (
this.host === '' || // Relative paths are always internal links...
(this.host === window.location.host && this.protocol === window.location.protocol && // Otherwise need to check that protocol and host match
(RELATIVE_PATH.length > 0 ? this.pathname.indexOf(RELATIVE_PATH) === 0 : true)) // Subfolder installs need this additional check
) {
// Internal link
var url = this.pathname.replace(RELATIVE_PATH + '/', '');
// Special handling for urls with hashes
if (window.location.pathname === this.pathname && this.hash.length) {
window.location.hash = this.hash;
} else {
window.location.hash = '';
if (ajaxify.go(url)) {
e.preventDefault();
}
}
} else if (window.location.pathname !== '/outgoing') {
// External Link
if (config.openOutgoingLinksInNewTab) {
window.open(this.href, '_blank');
e.preventDefault();
} else if (config.useOutgoingLinksPage) {
ajaxify.go('outgoing?url=' + encodeURIComponent(this.href));
e.preventDefault();
}
}
}
});
}
if (window.history && window.history.pushState) {
// Progressive Enhancement, ajaxify available only to modern browsers
ajaxifyAnchors();
}
app.load();
templates.cache['500'] = $('.tpl-500').html();
"use strict";
var ajaxify = ajaxify || {};
$(document).ready(function() {
/*global app, templates, utils, socket, config, RELATIVE_PATH*/
var location = document.location || window.location,
rootUrl = location.protocol + '//' + (location.hostname || location.host) + (location.port ? ':' + location.port : ''),
apiXHR = null,
translator;
// Dumb hack to fool ajaxify into thinking translator is still a global
// When ajaxify is migrated to a require.js module, then this can be merged into the "define" call
require(['translator'], function(_translator) {
translator = _translator;
});
$(window).on('popstate', function (ev) {
ev = ev.originalEvent;
if (ev !== null && ev.state && ev.state.url !== undefined) {
ajaxify.go(ev.state.url, function() {
$(window).trigger('action:popstate', {url: ev.state.url});
}, true);
}
});
ajaxify.currentPage = null;
ajaxify.go = function (url, callback, quiet, search) {
if (!socket.connected) {
if (ajaxify.reconnectAction) {
$(window).off('action:reconnected', ajaxify.reconnectAction);
}
ajaxify.reconnectAction = function(e) {
ajaxify.go(url, callback, quiet, search);
$(window).off(e);
}
$(window).on('action:reconnected', ajaxify.reconnectAction);
}
if (ajaxify.handleRedirects(url)) {
return true;
}
app.enterRoom('');
$(window).off('scroll');
if ($('#content').hasClass('ajaxifying') && apiXHR) {
apiXHR.abort();
}
url = ajaxify.start(url, quiet, search);
$('#footer, #content').removeClass('hide').addClass('ajaxifying');
ajaxify.variables.flush();
ajaxify.loadData(url, function(err, data) {
if (err) {
return onAjaxError(err, url, callback, quiet);
}
app.template = data.template.name;
require(['translator'], function(translator) {
translator.load(config.defaultLang, data.template.name);
renderTemplate(url, data.template.name, data, callback);
});
});
return true;
};
ajaxify.handleRedirects = function(url) {
url = ajaxify.removeRelativePath(url.replace(/\/$/, '')).toLowerCase();
var isAdminRoute = url.startsWith('admin') && window.location.pathname.indexOf(RELATIVE_PATH + '/admin') !== 0;
var uploadsOrApi = url.startsWith('uploads') || url.startsWith('api');
if (isAdminRoute || uploadsOrApi) {
window.open(RELATIVE_PATH + '/' + url, '_top');
return true;
}
return false;
};
ajaxify.start = function(url, quiet, search) {
url = ajaxify.removeRelativePath(url.replace(/^\/|\/$/g, ''));
var hash = window.location.hash;
search = search || '';
$(window).trigger('action:ajaxify.start', {url: url});
if (!window.location.pathname.match(/\/(403|404)$/g)) {
app.previousUrl = window.location.href;
}
ajaxify.currentPage = url;
if (window.history && window.history.pushState) {
window.history[!quiet ? 'pushState' : 'replaceState']({
url: url + search + hash
}, url, RELATIVE_PATH + '/' + url + search + hash);
}
return url;
};
function onAjaxError(err, url, callback, quiet) {
var data = err.data,
textStatus = err.textStatus;
if (data) {
var status = parseInt(data.status, 10);
if (status === 403 || status === 404 || status === 500 || status === 502) {
if (status === 502) {
status = 500;
}
$('#footer, #content').removeClass('hide').addClass('ajaxifying');
return renderTemplate(url, status.toString(), data.responseJSON, (new Date()).getTime(), callback);
} else if (status === 401) {
app.alertError('[[global:please_log_in]]');
app.previousUrl = url;
return ajaxify.go('login');
} else if (status === 302) {
if (data.responseJSON.external) {
window.location.href = data.responseJSON.external;
} else if (typeof data.responseJSON === 'string') {
ajaxify.go(data.responseJSON.slice(1), callback, quiet);
}
}
} else if (textStatus !== 'abort') {
app.alertError(data.responseJSON.error);
}
}
function renderTemplate(url, tpl_url, data, callback) {
$(window).trigger('action:ajaxify.loadingTemplates', {});
templates.parse(tpl_url, data, function(template) {
translator.translate(template, function(translatedTemplate) {
$('#content').html(translatedTemplate);
ajaxify.end(url, tpl_url);
if (typeof callback === 'function') {
callback();
}
$('#content, #footer').removeClass('ajaxifying');
app.refreshTitle(url);
});
});
}
ajaxify.end = function(url, tpl_url) {
function done() {
if (--count === 0) {
$(window).trigger('action:ajaxify.end', {url: url});
}
}
var count = 2;
ajaxify.variables.parse();
ajaxify.loadScript(tpl_url, done);
ajaxify.widgets.render(tpl_url, url, done);
$(window).trigger('action:ajaxify.contentLoaded', {url: url, tpl: tpl_url});
app.processPage();
};
ajaxify.removeRelativePath = function(url) {
if (url.startsWith(RELATIVE_PATH.slice(1))) {
url = url.slice(RELATIVE_PATH.length);
}
return url;
};
ajaxify.refresh = function(e) {
if (e && e instanceof jQuery.Event) {
e.preventDefault();
}
ajaxify.go(ajaxify.currentPage, null, true);
};
ajaxify.loadScript = function(tpl_url, callback) {
var location = !app.inAdmin ? 'forum/' : '';
require([location + tpl_url], function(script) {
if (script && script.init) {
script.init();
}
if (callback) {
callback();
}
});
};
ajaxify.loadData = function(url, callback) {
url = ajaxify.removeRelativePath(url);
$(window).trigger('action:ajaxify.loadingData', {url: url});
apiXHR = $.ajax({
url: RELATIVE_PATH + '/api/' + url,
cache: false,
success: function(data) {
if (!data) {
return;
}
ajaxify.data = data;
data.relative_path = RELATIVE_PATH;
$(window).trigger('action:ajaxify.dataLoaded', {url: url, data: data});
if (callback) {
callback(null, data);
}
},
error: function(data, textStatus) {
if (data.status === 0 && textStatus === 'error') {
data.status = 500;
}
callback({
data: data,
textStatus: textStatus
});
}
});
};
ajaxify.loadTemplate = function(template, callback) {
if (templates.cache[template]) {
callback(templates.cache[template]);
} else {
$.ajax({
url: RELATIVE_PATH + '/templates/' + template + '.tpl' + (config['cache-buster'] ? '?v=' + config['cache-buster'] : ''),
type: 'GET',
success: function(data) {
callback(data.toString());
},
error: function(error) {
throw new Error("Unable to load template: " + template + " (" + error.statusText + ")");
}
});
}
};
function ajaxifyAnchors() {
templates.registerLoader(ajaxify.loadTemplate);
function hrefEmpty(href) {
return href === undefined || href === '' || href === 'javascript:;';
}
// Enhancing all anchors to ajaxify...
$(document.body).on('click', 'a', function (e) {
if (this.target !== '' || (this.protocol !== 'http:' && this.protocol !== 'https:')) {
return;
} else if (hrefEmpty(this.href) || this.protocol === 'javascript:' || $(this).attr('data-ajaxify') === 'false' || $(this).attr('href') === '#') {
return e.preventDefault();
}
if (!e.ctrlKey && !e.shiftKey && !e.metaKey && e.which === 1) {
if (
this.host === '' || // Relative paths are always internal links...
(this.host === window.location.host && this.protocol === window.location.protocol && // Otherwise need to check that protocol and host match
(RELATIVE_PATH.length > 0 ? this.pathname.indexOf(RELATIVE_PATH) === 0 : true)) // Subfolder installs need this additional check
) {
// Internal link
var url = this.pathname.replace(RELATIVE_PATH + '/', '');
// Special handling for urls with hashes
if (window.location.pathname === this.pathname && this.hash.length) {
window.location.hash = this.hash;
} else {
window.location.hash = '';
if (ajaxify.go(url)) {
e.preventDefault();
}
}
} else if (window.location.pathname !== '/outgoing') {
// External Link
if (config.openOutgoingLinksInNewTab) {
window.open(this.href, '_blank');
e.preventDefault();
} else if (config.useOutgoingLinksPage) {
ajaxify.go('outgoing?url=' + encodeURIComponent(this.href));
e.preventDefault();
}
}
}
});
}
if (window.history && window.history.pushState) {
// Progressive Enhancement, ajaxify available only to modern browsers
ajaxifyAnchors();
}
app.load();
templates.cache['500'] = $('.tpl-500').html();
});

File diff suppressed because it is too large Load Diff

@ -1,29 +1,29 @@
*.pyc
*.egg-info
*.db
*.db.old
*.swp
*.db-journal
.coverage
.DS_Store
.installed.cfg
.idea/*
.svn/*
src/website/static/*
src/website/media/*
bin
build
cfcache
develop-eggs
dist
downloads
eggs
parts
*.pyc
*.egg-info
*.db
*.db.old
*.swp
*.db-journal
.coverage
.DS_Store
.installed.cfg
.idea/*
.svn/*
src/website/static/*
src/website/media/*
bin
build
cfcache
develop-eggs
dist
downloads
eggs
parts
tmp
.sass-cache
src/website/settingslocal.py
.sass-cache
src/website/settingslocal.py
stunnel.log

@ -1,22 +1,22 @@

// Persian
// Use DIR attribute for RTL text in Persian Language for ABBR tag .
// By MB.seifollahi@gmail.com
jQuery.timeago.settings.strings = {
prefixAgo: null,
prefixFromNow: null,
suffixAgo: "پیش",
suffixFromNow: "از حال",
seconds: "کمتر از یک دقیقه",
minute: "حدود یک دقیقه",
minutes: "%d دقیقه",
hour: "حدود یک ساعت",
hours: "حدود %d ساعت",
day: "یک روز",
days: "%d روز",
month: "حدود یک ماه",
months: "%d ماه",
year: "حدود یک سال",
years: "%d سال",
wordSeparator: " "
// Persian
// Use DIR attribute for RTL text in Persian Language for ABBR tag .
// By MB.seifollahi@gmail.com
jQuery.timeago.settings.strings = {
prefixAgo: null,
prefixFromNow: null,
suffixAgo: "پیش",
suffixFromNow: "از حال",
seconds: "کمتر از یک دقیقه",
minute: "حدود یک دقیقه",
minutes: "%d دقیقه",
hour: "حدود یک ساعت",
hours: "حدود %d ساعت",
day: "یک روز",
days: "%d روز",
month: "حدود یک ماه",
months: "%d ماه",
year: "حدود یک سال",
years: "%d سال",
wordSeparator: " "
};

@ -1,19 +1,19 @@
//Uzbek
jQuery.timeago.settings.strings = {
prefixAgo: null,
prefixFromNow: "keyin",
suffixAgo: "avval",
suffixFromNow: null,
seconds: "bir necha soniya",
minute: "1 daqiqa",
minutes: function(value) { return "%d daqiqa" },
hour: "1 soat",
hours: function(value) { return "%d soat" },
day: "1 kun",
days: function(value) { return "%d kun" },
month: "1 oy",
months: function(value) { return "%d oy" },
year: "1 yil",
years: function(value) { return "%d yil" },
wordSeparator: " "
};
//Uzbek
jQuery.timeago.settings.strings = {
prefixAgo: null,
prefixFromNow: "keyin",
suffixAgo: "avval",
suffixFromNow: null,
seconds: "bir necha soniya",
minute: "1 daqiqa",
minutes: function(value) { return "%d daqiqa" },
hour: "1 soat",
hours: function(value) { return "%d soat" },
day: "1 kun",
days: function(value) { return "%d kun" },
month: "1 oy",
months: function(value) { return "%d oy" },
year: "1 yil",
years: function(value) { return "%d yil" },
wordSeparator: " "
};

@ -1,85 +1,85 @@
"use strict";
var express = require('express');
function apiRoutes(router, middleware, controllers) {
router.get('/users/csv', middleware.authenticate, controllers.admin.users.getCSV);
var multipart = require('connect-multiparty');
var multipartMiddleware = multipart();
var middlewares = [multipartMiddleware, middleware.validateFiles, middleware.applyCSRF, middleware.authenticate];
router.post('/category/uploadpicture', middlewares, controllers.admin.uploads.uploadCategoryPicture);
router.post('/uploadfavicon', middlewares, controllers.admin.uploads.uploadFavicon);
router.post('/uploadlogo', middlewares, controllers.admin.uploads.uploadLogo);
router.post('/uploadgravatardefault', middlewares, controllers.admin.uploads.uploadGravatarDefault);
}
function adminRouter(middleware, controllers) {
var router = express.Router();
router.use(middleware.admin.buildHeader);
addRoutes(router, middleware, controllers);
return router;
}
function apiRouter(middleware, controllers) {
var router = express.Router();
addRoutes(router, middleware, controllers);
apiRoutes(router, middleware, controllers);
return router;
}
function addRoutes(router, middleware, controllers) {
router.get('/', controllers.admin.home);
router.get('/general/dashboard', controllers.admin.home);
router.get('/general/languages', controllers.admin.languages.get);
router.get('/general/sounds', controllers.admin.sounds.get);
router.get('/general/navigation', controllers.admin.navigation.get);
router.get('/general/homepage', controllers.admin.homepage.get);
router.get('/manage/categories', controllers.admin.categories.getAll);
router.get('/manage/categories/:category_id', controllers.admin.categories.get);
router.get('/manage/tags', controllers.admin.tags.get);
router.get('/manage/flags', controllers.admin.flags.get);
router.get('/manage/users', controllers.admin.users.sortByJoinDate);
router.get('/manage/users/search', controllers.admin.users.search);
router.get('/manage/users/latest', controllers.admin.users.sortByJoinDate);
router.get('/manage/users/sort-posts', controllers.admin.users.sortByPosts);
router.get('/manage/users/sort-reputation', controllers.admin.users.sortByReputation);
router.get('/manage/users/banned', controllers.admin.users.banned);
router.get('/manage/users/registration', controllers.admin.users.registrationQueue);
router.get('/manage/groups', controllers.admin.groups.list);
router.get('/manage/groups/:name', controllers.admin.groups.get);
router.get('/settings/:term?', controllers.admin.settings.get);
router.get('/appearance/:term?', controllers.admin.appearance.get);
router.get('/extend/plugins', controllers.admin.plugins.get);
router.get('/extend/widgets', controllers.admin.extend.widgets);
router.get('/extend/rewards', controllers.admin.extend.rewards);
router.get('/advanced/database', controllers.admin.database.get);
router.get('/advanced/events', controllers.admin.events.get);
router.get('/advanced/logs', controllers.admin.logs.get);
router.get('/advanced/post-cache', controllers.admin.postCache.get);
router.get('/development/logger', controllers.admin.logger.get);
}
module.exports = function(app, middleware, controllers) {
app.use('/admin/', adminRouter(middleware, controllers));
app.use('/api/admin/', apiRouter(middleware, controllers));
};
"use strict";
var express = require('express');
function apiRoutes(router, middleware, controllers) {
router.get('/users/csv', middleware.authenticate, controllers.admin.users.getCSV);
var multipart = require('connect-multiparty');
var multipartMiddleware = multipart();
var middlewares = [multipartMiddleware, middleware.validateFiles, middleware.applyCSRF, middleware.authenticate];
router.post('/category/uploadpicture', middlewares, controllers.admin.uploads.uploadCategoryPicture);
router.post('/uploadfavicon', middlewares, controllers.admin.uploads.uploadFavicon);
router.post('/uploadlogo', middlewares, controllers.admin.uploads.uploadLogo);
router.post('/uploadgravatardefault', middlewares, controllers.admin.uploads.uploadGravatarDefault);
}
function adminRouter(middleware, controllers) {
var router = express.Router();
router.use(middleware.admin.buildHeader);
addRoutes(router, middleware, controllers);
return router;
}
function apiRouter(middleware, controllers) {
var router = express.Router();
addRoutes(router, middleware, controllers);
apiRoutes(router, middleware, controllers);
return router;
}
function addRoutes(router, middleware, controllers) {
router.get('/', controllers.admin.home);
router.get('/general/dashboard', controllers.admin.home);
router.get('/general/languages', controllers.admin.languages.get);
router.get('/general/sounds', controllers.admin.sounds.get);
router.get('/general/navigation', controllers.admin.navigation.get);
router.get('/general/homepage', controllers.admin.homepage.get);
router.get('/manage/categories', controllers.admin.categories.getAll);
router.get('/manage/categories/:category_id', controllers.admin.categories.get);
router.get('/manage/tags', controllers.admin.tags.get);
router.get('/manage/flags', controllers.admin.flags.get);
router.get('/manage/users', controllers.admin.users.sortByJoinDate);
router.get('/manage/users/search', controllers.admin.users.search);
router.get('/manage/users/latest', controllers.admin.users.sortByJoinDate);
router.get('/manage/users/sort-posts', controllers.admin.users.sortByPosts);
router.get('/manage/users/sort-reputation', controllers.admin.users.sortByReputation);
router.get('/manage/users/banned', controllers.admin.users.banned);
router.get('/manage/users/registration', controllers.admin.users.registrationQueue);
router.get('/manage/groups', controllers.admin.groups.list);
router.get('/manage/groups/:name', controllers.admin.groups.get);
router.get('/settings/:term?', controllers.admin.settings.get);
router.get('/appearance/:term?', controllers.admin.appearance.get);
router.get('/extend/plugins', controllers.admin.plugins.get);
router.get('/extend/widgets', controllers.admin.extend.widgets);
router.get('/extend/rewards', controllers.admin.extend.rewards);
router.get('/advanced/database', controllers.admin.database.get);
router.get('/advanced/events', controllers.admin.events.get);
router.get('/advanced/logs', controllers.admin.logs.get);
router.get('/advanced/post-cache', controllers.admin.postCache.get);
router.get('/development/logger', controllers.admin.logger.get);
}
module.exports = function(app, middleware, controllers) {
app.use('/admin/', adminRouter(middleware, controllers));
app.use('/api/admin/', apiRouter(middleware, controllers));
};

@ -1,86 +1,86 @@
(function(Auth) {
"use strict";
var passport = require('passport'),
passportLocal = require('passport-local').Strategy,
nconf = require('nconf'),
winston = require('winston'),
express = require('express'),
controllers = require('../controllers'),
plugins = require('../plugins'),
hotswap = require('../hotswap'),
loginStrategies = [];
Auth.initialize = function(app, middleware) {
app.use(passport.initialize());
app.use(passport.session());
app.use(function(req, res, next) {
req.uid = req.user ? parseInt(req.user.uid, 10) : 0;
next();
});
Auth.app = app;
Auth.middleware = middleware;
};
Auth.getLoginStrategies = function() {
return loginStrategies;
};
Auth.reloadRoutes = function(callback) {
var router = express.Router();
router.hotswapId = 'auth';
loginStrategies.length = 0;
if (plugins.hasListeners('action:auth.overrideLogin')) {
winston.warn('[authentication] Login override detected, skipping local login strategy.');
plugins.fireHook('action:auth.overrideLogin');
} else {
passport.use(new passportLocal({passReqToCallback: true}, controllers.authentication.localLogin));
}
plugins.fireHook('filter:auth.init', loginStrategies, function(err) {
if (err) {
winston.error('filter:auth.init - plugin failure');
return callback(err);
}
loginStrategies.forEach(function(strategy) {
if (strategy.url) {
router.get(strategy.url, passport.authenticate(strategy.name, {
scope: strategy.scope
}));
}
router.get(strategy.callbackURL, passport.authenticate(strategy.name, {
successReturnToOrRedirect: nconf.get('relative_path') + (strategy.successUrl !== undefined ? strategy.successUrl : '/'),
failureRedirect: nconf.get('relative_path') + (strategy.failureUrl !== undefined ? strategy.failureUrl : '/login')
}));
});
router.post('/register', Auth.middleware.applyCSRF, controllers.authentication.register);
router.post('/login', Auth.middleware.applyCSRF, controllers.authentication.login);
router.post('/logout', Auth.middleware.applyCSRF, controllers.authentication.logout);
hotswap.replace('auth', router);
if (typeof callback === 'function') {
callback();
}
});
};
passport.serializeUser(function(user, done) {
done(null, user.uid);
});
passport.deserializeUser(function(uid, done) {
done(null, {
uid: uid
});
});
}(exports));
(function(Auth) {
"use strict";
var passport = require('passport'),
passportLocal = require('passport-local').Strategy,
nconf = require('nconf'),
winston = require('winston'),
express = require('express'),
controllers = require('../controllers'),
plugins = require('../plugins'),
hotswap = require('../hotswap'),
loginStrategies = [];
Auth.initialize = function(app, middleware) {
app.use(passport.initialize());
app.use(passport.session());
app.use(function(req, res, next) {
req.uid = req.user ? parseInt(req.user.uid, 10) : 0;
next();
});
Auth.app = app;
Auth.middleware = middleware;
};
Auth.getLoginStrategies = function() {
return loginStrategies;
};
Auth.reloadRoutes = function(callback) {
var router = express.Router();
router.hotswapId = 'auth';
loginStrategies.length = 0;
if (plugins.hasListeners('action:auth.overrideLogin')) {
winston.warn('[authentication] Login override detected, skipping local login strategy.');
plugins.fireHook('action:auth.overrideLogin');
} else {
passport.use(new passportLocal({passReqToCallback: true}, controllers.authentication.localLogin));
}
plugins.fireHook('filter:auth.init', loginStrategies, function(err) {
if (err) {
winston.error('filter:auth.init - plugin failure');
return callback(err);
}
loginStrategies.forEach(function(strategy) {
if (strategy.url) {
router.get(strategy.url, passport.authenticate(strategy.name, {
scope: strategy.scope
}));
}
router.get(strategy.callbackURL, passport.authenticate(strategy.name, {
successReturnToOrRedirect: nconf.get('relative_path') + (strategy.successUrl !== undefined ? strategy.successUrl : '/'),
failureRedirect: nconf.get('relative_path') + (strategy.failureUrl !== undefined ? strategy.failureUrl : '/login')
}));
});
router.post('/register', Auth.middleware.applyCSRF, controllers.authentication.register);
router.post('/login', Auth.middleware.applyCSRF, controllers.authentication.login);
router.post('/logout', Auth.middleware.applyCSRF, controllers.authentication.logout);
hotswap.replace('auth', router);
if (typeof callback === 'function') {
callback();
}
});
};
passport.serializeUser(function(user, done) {
done(null, user.uid);
});
passport.deserializeUser(function(uid, done) {
done(null, {
uid: uid
});
});
}(exports));

@ -1,379 +1,379 @@
"use strict";
var async = require('async'),
validator = require('validator'),
_ = require('underscore'),
db = require('./database'),
posts = require('./posts'),
utils = require('../public/src/utils'),
plugins = require('./plugins'),
user = require('./user'),
categories = require('./categories'),
privileges = require('./privileges');
(function(Topics) {
require('./topics/create')(Topics);
require('./topics/delete')(Topics);
require('./topics/unread')(Topics);
require('./topics/recent')(Topics);
require('./topics/popular')(Topics);
require('./topics/user')(Topics);
require('./topics/fork')(Topics);
require('./topics/posts')(Topics);
require('./topics/follow')(Topics);
require('./topics/tags')(Topics);
require('./topics/teaser')(Topics);
require('./topics/suggested')(Topics);
Topics.exists = function(tid, callback) {
db.isSortedSetMember('topics:tid', tid, callback);
};
Topics.getTopicData = function(tid, callback) {
db.getObject('topic:' + tid, function(err, topic) {
if (err || !topic) {
return callback(err);
}
modifyTopic(topic, callback);
});
};
Topics.getTopicsData = function(tids, callback) {
var keys = [];
for (var i=0; i<tids.length; ++i) {
keys.push('topic:' + tids[i]);
}
db.getObjects(keys, function(err, topics) {
if (err) {
return callback(err);
}
async.map(topics, modifyTopic, callback);
});
};
function modifyTopic(topic, callback) {
if (!topic) {
return callback(null, topic);
}
topic.title = validator.escape(topic.title);
topic.relativeTime = utils.toISOString(topic.timestamp);
topic.lastposttimeISO = utils.toISOString(topic.lastposttime);
callback(null, topic);
}
Topics.getPageCount = function(tid, uid, callback) {
Topics.getTopicField(tid, 'postcount', function(err, postCount) {
if (err) {
return callback(err);
}
if (!parseInt(postCount, 10)) {
return callback(null, 1);
}
user.getSettings(uid, function(err, settings) {
if (err) {
return callback(err);
}
callback(null, Math.ceil((parseInt(postCount, 10) - 1) / settings.postsPerPage));
});
});
};
Topics.getTidPage = function(tid, uid, callback) {
if(!tid) {
return callback(new Error('[[error:invalid-tid]]'));
}
async.parallel({
index: function(next) {
categories.getTopicIndex(tid, next);
},
settings: function(next) {
user.getSettings(uid, next);
}
}, function(err, results) {
if(err) {
return callback(err);
}
callback(null, Math.ceil((results.index + 1) / results.settings.topicsPerPage));
});
};
Topics.getCategoryData = function(tid, callback) {
Topics.getTopicField(tid, 'cid', function(err, cid) {
if (err) {
callback(err);
}
categories.getCategoryData(cid, callback);
});
};
Topics.getTopicsFromSet = function(set, uid, start, stop, callback) {
async.waterfall([
function(next) {
db.getSortedSetRevRange(set, start, stop, next);
},
function(tids, next) {
Topics.getTopics(tids, uid, next);
},
function(topics, next) {
next(null, {topics: topics, nextStart: stop + 1});
}
], callback);
};
Topics.getTopics = function(tids, uid, callback) {
async.waterfall([
function(next) {
privileges.topics.filterTids('read', tids, uid, next);
},
function(tids, next) {
Topics.getTopicsByTids(tids, uid, next);
}
], callback);
};
Topics.getTopicsByTids = function(tids, uid, callback) {
if (!Array.isArray(tids) || !tids.length) {
return callback(null, []);
}
Topics.getTopicsData(tids, function(err, topics) {
function mapFilter(array, field) {
return array.map(function(topic) {
return topic && topic[field] && topic[field].toString();
}).filter(function(value, index, array) {
return utils.isNumber(value) && array.indexOf(value) === index;
});
}
if (err) {
return callback(err);
}
var uids = mapFilter(topics, 'uid');
var cids = mapFilter(topics, 'cid');
async.parallel({
teasers: function(next) {
Topics.getTeasers(topics, next);
},
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);
},
tags: function(next) {
Topics.getTopicsTagsObjects(tids, next);
}
}, function(err, results) {
if (err) {
return callback(err);
}
var users = _.object(uids, results.users);
var categories = _.object(cids, results.categories);
for (var i=0; i<topics.length; ++i) {
if (topics[i]) {
topics[i].category = categories[topics[i].cid];
topics[i].user = users[topics[i].uid];
topics[i].teaser = results.teasers[i];
topics[i].tags = results.tags[i];
topics[i].isOwner = parseInt(topics[i].uid, 10) === parseInt(uid, 10);
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];
topics[i].unreplied = parseInt(topics[i].postcount, 10) <= 1;
}
}
topics = topics.filter(function(topic) {
return topic && topic.category && !topic.category.disabled;
});
plugins.fireHook('filter:topics.get', {topics: topics, uid: uid}, function(err, topicData) {
callback(err, topicData.topics);
});
});
});
};
Topics.getTopicWithPosts = function(tid, set, uid, start, stop, reverse, callback) {
Topics.getTopicData(tid, function(err, topicData) {
if (err || !topicData) {
return callback(err || new Error('[[error:no-topic]]'));
}
async.parallel({
posts: async.apply(getMainPostAndReplies, topicData, set, uid, start, stop, reverse),
category: async.apply(Topics.getCategoryData, tid),
threadTools: async.apply(plugins.fireHook, 'filter:topic.thread_tools', {topic: topicData, uid: uid, tools: []}),
tags: async.apply(Topics.getTopicTagsObjects, tid),
isFollowing: async.apply(Topics.isFollowing, [tid], uid),
bookmark: async.apply(Topics.getUserBookmark, tid, uid)
}, function(err, results) {
if (err) {
return callback(err);
}
topicData.posts = results.posts;
topicData.category = results.category;
topicData.thread_tools = results.threadTools.tools;
topicData.tags = results.tags;
topicData.isFollowing = results.isFollowing[0];
topicData.bookmark = results.bookmark;
topicData.unreplied = parseInt(topicData.postcount, 10) === 1;
topicData.deleted = parseInt(topicData.deleted, 10) === 1;
topicData.locked = parseInt(topicData.locked, 10) === 1;
topicData.pinned = parseInt(topicData.pinned, 10) === 1;
plugins.fireHook('filter:topic.get', {topic: topicData, uid: uid}, function(err, data) {
callback(err, data ? data.topic : null);
});
});
});
};
function getMainPostAndReplies(topic, set, uid, start, stop, reverse, callback) {
async.waterfall([
function(next) {
posts.getPidsFromSet(set, start, stop, reverse, next);
},
function(pids, next) {
if ((!Array.isArray(pids) || !pids.length) && !topic.mainPid) {
return callback(null, []);
}
if (topic.mainPid) {
pids.unshift(topic.mainPid);
}
posts.getPostsByPids(pids, uid, next);
},
function(posts, next) {
if (!posts.length) {
return next(null, []);
}
if (topic.mainPid) {
posts[0].index = 0;
}
var indices = Topics.calculatePostIndices(start, stop, topic.postcount, reverse);
for (var i=1; i<posts.length; ++i) {
if (posts[i]) {
posts[i].index = indices[i - 1];
}
}
Topics.addPostData(posts, uid, callback);
}
]);
}
Topics.getMainPost = function(tid, uid, callback) {
Topics.getMainPosts([tid], uid, function(err, mainPosts) {
callback(err, Array.isArray(mainPosts) && mainPosts.length ? mainPosts[0] : null);
});
};
Topics.getMainPids = function(tids, callback) {
if (!Array.isArray(tids) || !tids.length) {
return callback(null, []);
}
Topics.getTopicsFields(tids, ['mainPid'], function(err, topicData) {
if (err) {
return callback(err);
}
var mainPids = topicData.map(function(topic) {
return topic && topic.mainPid;
});
callback(null, mainPids);
});
};
Topics.getMainPosts = function(tids, uid, callback) {
Topics.getMainPids(tids, function(err, mainPids) {
if (err) {
return callback(err);
}
getMainPosts(mainPids, uid, callback);
});
};
function getMainPosts(mainPids, uid, callback) {
posts.getPostsByPids(mainPids, uid, function(err, postData) {
if (err) {
return callback(err);
}
postData.forEach(function(post) {
if (post) {
post.index = 0;
}
});
Topics.addPostData(postData, uid, callback);
});
}
Topics.getUserBookmark = function (tid, uid, callback) {
Topics.getTopicField(tid + ':bookmarks', uid, callback);
}
Topics.setUserBookmark = function(data, callback) {
Topics.setTopicField(data.tid + ':bookmarks', data.uid, data.postIndex, callback);
}
Topics.getTopicField = function(tid, field, callback) {
db.getObjectField('topic:' + tid, field, callback);
};
Topics.getTopicFields = function(tid, fields, callback) {
db.getObjectFields('topic:' + tid, fields, callback);
};
Topics.getTopicsFields = function(tids, fields, callback) {
if (!Array.isArray(tids) || !tids.length) {
return callback(null, []);
}
var keys = tids.map(function(tid) {
return 'topic:' + tid;
});
db.getObjectsFields(keys, fields, callback);
};
Topics.setTopicField = function(tid, field, value, callback) {
db.setObjectField('topic:' + tid, field, value, callback);
};
Topics.isLocked = function(tid, callback) {
Topics.getTopicField(tid, 'locked', function(err, locked) {
callback(err, parseInt(locked, 10) === 1);
});
};
Topics.search = function(tid, term, callback) {
if (plugins.hasListeners('filter:topic.search')) {
plugins.fireHook('filter:topic.search', {
tid: tid,
term: term
}, callback);
} else {
callback(new Error('no-plugins-available'), []);
}
};
}(exports));
"use strict";
var async = require('async'),
validator = require('validator'),
_ = require('underscore'),
db = require('./database'),
posts = require('./posts'),
utils = require('../public/src/utils'),
plugins = require('./plugins'),
user = require('./user'),
categories = require('./categories'),
privileges = require('./privileges');
(function(Topics) {
require('./topics/create')(Topics);
require('./topics/delete')(Topics);
require('./topics/unread')(Topics);
require('./topics/recent')(Topics);
require('./topics/popular')(Topics);
require('./topics/user')(Topics);
require('./topics/fork')(Topics);
require('./topics/posts')(Topics);
require('./topics/follow')(Topics);
require('./topics/tags')(Topics);
require('./topics/teaser')(Topics);
require('./topics/suggested')(Topics);
Topics.exists = function(tid, callback) {
db.isSortedSetMember('topics:tid', tid, callback);
};
Topics.getTopicData = function(tid, callback) {
db.getObject('topic:' + tid, function(err, topic) {
if (err || !topic) {
return callback(err);
}
modifyTopic(topic, callback);
});
};
Topics.getTopicsData = function(tids, callback) {
var keys = [];
for (var i=0; i<tids.length; ++i) {
keys.push('topic:' + tids[i]);
}
db.getObjects(keys, function(err, topics) {
if (err) {
return callback(err);
}
async.map(topics, modifyTopic, callback);
});
};
function modifyTopic(topic, callback) {
if (!topic) {
return callback(null, topic);
}
topic.title = validator.escape(topic.title);
topic.relativeTime = utils.toISOString(topic.timestamp);
topic.lastposttimeISO = utils.toISOString(topic.lastposttime);
callback(null, topic);
}
Topics.getPageCount = function(tid, uid, callback) {
Topics.getTopicField(tid, 'postcount', function(err, postCount) {
if (err) {
return callback(err);
}
if (!parseInt(postCount, 10)) {
return callback(null, 1);
}
user.getSettings(uid, function(err, settings) {
if (err) {
return callback(err);
}
callback(null, Math.ceil((parseInt(postCount, 10) - 1) / settings.postsPerPage));
});
});
};
Topics.getTidPage = function(tid, uid, callback) {
if(!tid) {
return callback(new Error('[[error:invalid-tid]]'));
}
async.parallel({
index: function(next) {
categories.getTopicIndex(tid, next);
},
settings: function(next) {
user.getSettings(uid, next);
}
}, function(err, results) {
if(err) {
return callback(err);
}
callback(null, Math.ceil((results.index + 1) / results.settings.topicsPerPage));
});
};
Topics.getCategoryData = function(tid, callback) {
Topics.getTopicField(tid, 'cid', function(err, cid) {
if (err) {
callback(err);
}
categories.getCategoryData(cid, callback);
});
};
Topics.getTopicsFromSet = function(set, uid, start, stop, callback) {
async.waterfall([
function(next) {
db.getSortedSetRevRange(set, start, stop, next);
},
function(tids, next) {
Topics.getTopics(tids, uid, next);
},
function(topics, next) {
next(null, {topics: topics, nextStart: stop + 1});
}
], callback);
};
Topics.getTopics = function(tids, uid, callback) {
async.waterfall([
function(next) {
privileges.topics.filterTids('read', tids, uid, next);
},
function(tids, next) {
Topics.getTopicsByTids(tids, uid, next);
}
], callback);
};
Topics.getTopicsByTids = function(tids, uid, callback) {
if (!Array.isArray(tids) || !tids.length) {
return callback(null, []);
}
Topics.getTopicsData(tids, function(err, topics) {
function mapFilter(array, field) {
return array.map(function(topic) {
return topic && topic[field] && topic[field].toString();
}).filter(function(value, index, array) {
return utils.isNumber(value) && array.indexOf(value) === index;
});
}
if (err) {
return callback(err);
}
var uids = mapFilter(topics, 'uid');
var cids = mapFilter(topics, 'cid');
async.parallel({
teasers: function(next) {
Topics.getTeasers(topics, next);
},
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);
},
tags: function(next) {
Topics.getTopicsTagsObjects(tids, next);
}
}, function(err, results) {
if (err) {
return callback(err);
}
var users = _.object(uids, results.users);
var categories = _.object(cids, results.categories);
for (var i=0; i<topics.length; ++i) {
if (topics[i]) {
topics[i].category = categories[topics[i].cid];
topics[i].user = users[topics[i].uid];
topics[i].teaser = results.teasers[i];
topics[i].tags = results.tags[i];
topics[i].isOwner = parseInt(topics[i].uid, 10) === parseInt(uid, 10);
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];
topics[i].unreplied = parseInt(topics[i].postcount, 10) <= 1;
}
}
topics = topics.filter(function(topic) {
return topic && topic.category && !topic.category.disabled;
});
plugins.fireHook('filter:topics.get', {topics: topics, uid: uid}, function(err, topicData) {
callback(err, topicData.topics);
});
});
});
};
Topics.getTopicWithPosts = function(tid, set, uid, start, stop, reverse, callback) {
Topics.getTopicData(tid, function(err, topicData) {
if (err || !topicData) {
return callback(err || new Error('[[error:no-topic]]'));
}
async.parallel({
posts: async.apply(getMainPostAndReplies, topicData, set, uid, start, stop, reverse),
category: async.apply(Topics.getCategoryData, tid),
threadTools: async.apply(plugins.fireHook, 'filter:topic.thread_tools', {topic: topicData, uid: uid, tools: []}),
tags: async.apply(Topics.getTopicTagsObjects, tid),
isFollowing: async.apply(Topics.isFollowing, [tid], uid),
bookmark: async.apply(Topics.getUserBookmark, tid, uid)
}, function(err, results) {
if (err) {
return callback(err);
}
topicData.posts = results.posts;
topicData.category = results.category;
topicData.thread_tools = results.threadTools.tools;
topicData.tags = results.tags;
topicData.isFollowing = results.isFollowing[0];
topicData.bookmark = results.bookmark;
topicData.unreplied = parseInt(topicData.postcount, 10) === 1;
topicData.deleted = parseInt(topicData.deleted, 10) === 1;
topicData.locked = parseInt(topicData.locked, 10) === 1;
topicData.pinned = parseInt(topicData.pinned, 10) === 1;
plugins.fireHook('filter:topic.get', {topic: topicData, uid: uid}, function(err, data) {
callback(err, data ? data.topic : null);
});
});
});
};
function getMainPostAndReplies(topic, set, uid, start, stop, reverse, callback) {
async.waterfall([
function(next) {
posts.getPidsFromSet(set, start, stop, reverse, next);
},
function(pids, next) {
if ((!Array.isArray(pids) || !pids.length) && !topic.mainPid) {
return callback(null, []);
}
if (topic.mainPid) {
pids.unshift(topic.mainPid);
}
posts.getPostsByPids(pids, uid, next);
},
function(posts, next) {
if (!posts.length) {
return next(null, []);
}
if (topic.mainPid) {
posts[0].index = 0;
}
var indices = Topics.calculatePostIndices(start, stop, topic.postcount, reverse);
for (var i=1; i<posts.length; ++i) {
if (posts[i]) {
posts[i].index = indices[i - 1];
}
}
Topics.addPostData(posts, uid, callback);
}
]);
}
Topics.getMainPost = function(tid, uid, callback) {
Topics.getMainPosts([tid], uid, function(err, mainPosts) {
callback(err, Array.isArray(mainPosts) && mainPosts.length ? mainPosts[0] : null);
});
};
Topics.getMainPids = function(tids, callback) {
if (!Array.isArray(tids) || !tids.length) {
return callback(null, []);
}
Topics.getTopicsFields(tids, ['mainPid'], function(err, topicData) {
if (err) {
return callback(err);
}
var mainPids = topicData.map(function(topic) {
return topic && topic.mainPid;
});
callback(null, mainPids);
});
};
Topics.getMainPosts = function(tids, uid, callback) {
Topics.getMainPids(tids, function(err, mainPids) {
if (err) {
return callback(err);
}
getMainPosts(mainPids, uid, callback);
});
};
function getMainPosts(mainPids, uid, callback) {
posts.getPostsByPids(mainPids, uid, function(err, postData) {
if (err) {
return callback(err);
}
postData.forEach(function(post) {
if (post) {
post.index = 0;
}
});
Topics.addPostData(postData, uid, callback);
});
}
Topics.getUserBookmark = function (tid, uid, callback) {
Topics.getTopicField(tid + ':bookmarks', uid, callback);
}
Topics.setUserBookmark = function(data, callback) {
Topics.setTopicField(data.tid + ':bookmarks', data.uid, data.postIndex, callback);
}
Topics.getTopicField = function(tid, field, callback) {
db.getObjectField('topic:' + tid, field, callback);
};
Topics.getTopicFields = function(tid, fields, callback) {
db.getObjectFields('topic:' + tid, fields, callback);
};
Topics.getTopicsFields = function(tids, fields, callback) {
if (!Array.isArray(tids) || !tids.length) {
return callback(null, []);
}
var keys = tids.map(function(tid) {
return 'topic:' + tid;
});
db.getObjectsFields(keys, fields, callback);
};
Topics.setTopicField = function(tid, field, value, callback) {
db.setObjectField('topic:' + tid, field, value, callback);
};
Topics.isLocked = function(tid, callback) {
Topics.getTopicField(tid, 'locked', function(err, locked) {
callback(err, parseInt(locked, 10) === 1);
});
};
Topics.search = function(tid, term, callback) {
if (plugins.hasListeners('filter:topic.search')) {
plugins.fireHook('filter:topic.search', {
tid: tid,
term: term
}, callback);
} else {
callback(new Error('no-plugins-available'), []);
}
};
}(exports));

@ -1,230 +1,230 @@
'use strict';
var path = require('path'),
fs = require('fs'),
nconf = require('nconf'),
express = require('express'),
app = express(),
server,
winston = require('winston'),
async = require('async'),
emailer = require('./emailer'),
meta = require('./meta'),
logger = require('./logger'),
plugins = require('./plugins'),
middleware = require('./middleware'),
routes = require('./routes'),
emitter = require('./emitter'),
helpers = require('../public/src/modules/helpers');
if (nconf.get('ssl')) {
server = require('https').createServer({
key: fs.readFileSync(nconf.get('ssl').key),
cert: fs.readFileSync(nconf.get('ssl').cert)
}, app);
} else {
server = require('http').createServer(app);
}
module.exports.server = server;
server.on('error', function(err) {
winston.error(err);
if (err.code === 'EADDRINUSE') {
winston.error('NodeBB address in use, exiting...');
process.exit(0);
} else {
throw err;
}
});
module.exports.listen = function() {
emailer.registerApp(app);
middleware = middleware(app);
helpers.register();
logger.init(app);
emitter.all(['templates:compiled', 'meta:js.compiled', 'meta:css.compiled'], function() {
winston.info('NodeBB Ready');
emitter.emit('nodebb:ready');
listen();
});
initializeNodeBB(function(err) {
if (err) {
winston.error(err);
process.exit();
}
if (process.send) {
process.send({
action: 'ready'
});
}
});
};
function initializeNodeBB(callback) {
var skipJS, skipLess, fromFile = nconf.get('from-file') || '';
if (fromFile.match('js')) {
winston.info('[minifier] Minifying client-side JS skipped');
skipJS = true;
}
if (fromFile.match('less')) {
winston.info('[minifier] Compiling LESS files skipped');
skipLess = true;
}
async.waterfall([
async.apply(cacheStaticFiles),
async.apply(meta.themes.setupPaths),
function(next) {
plugins.init(app, middleware, next);
},
function(next) {
async.parallel([
async.apply(meta.templates.compile),
async.apply(!skipJS ? meta.js.minify : meta.js.getFromFile, app.enabled('minification')),
async.apply(!skipLess ? meta.css.minify : meta.css.getFromFile),
async.apply(meta.sounds.init)
], next);
},
function(results, next) {
plugins.fireHook('static:app.preload', {
app: app,
middleware: middleware
}, next);
},
function(next) {
routes(app, middleware);
next();
}
], callback);
}
function cacheStaticFiles(callback) {
if (global.env === 'development') {
return callback();
}
app.enable('cache');
app.enable('minification');
// Configure cache-buster timestamp
require('child_process').exec('git describe --tags', {
cwd: path.join(__dirname, '../')
}, function(err, stdOut) {
if (!err) {
meta.config['cache-buster'] = stdOut.trim();
callback();
} else {
fs.stat(path.join(__dirname, '../package.json'), function(err, stats) {
if (err) {
return callback(err);
}
meta.config['cache-buster'] = new Date(stats.mtime).getTime();
callback();
});
}
});
}
function listen(callback) {
var port = nconf.get('port');
if (Array.isArray(port)) {
if (!port.length) {
winston.error('[startup] empty ports array in config.json');
process.exit();
}
winston.warn('[startup] If you want to start nodebb on multiple ports please use loader.js');
winston.warn('[startup] Defaulting to first port in array, ' + port[0]);
port = port[0];
if (!port) {
winston.error('[startup] Invalid port, exiting');
process.exit();
}
}
if (port !== 80 && port !== 443 && nconf.get('use_port') === false) {
winston.info('Enabling \'trust proxy\'');
app.enable('trust proxy');
}
if ((port === 80 || port === 443) && process.env.NODE_ENV !== 'development') {
winston.info('Using ports 80 and 443 is not recommend; use a proxy instead. See README.md');
}
var isSocket = isNaN(port),
args = isSocket ? [port] : [port, nconf.get('bind_address')],
bind_address = ((nconf.get('bind_address') === "0.0.0.0" || !nconf.get('bind_address')) ? '0.0.0.0' : nconf.get('bind_address')) + ':' + port,
oldUmask;
args.push(function(err) {
if (err) {
winston.info('[startup] NodeBB was unable to listen on: ' + bind_address);
process.exit();
}
winston.info('NodeBB is now listening on: ' + (isSocket ? port : bind_address));
if (oldUmask) {
process.umask(oldUmask);
}
});
// Alter umask if necessary
if (isSocket) {
oldUmask = process.umask('0000');
module.exports.testSocket(port, function(err) {
if (!err) {
server.listen.apply(server, args);
} else {
winston.error('[startup] NodeBB was unable to secure domain socket access (' + port + ')');
winston.error('[startup] ' + err.message);
process.exit();
}
});
} else {
server.listen.apply(server, args);
}
}
module.exports.testSocket = function(socketPath, callback) {
if (typeof socketPath !== 'string') {
return callback(new Error('invalid socket path : ' + socketPath));
}
var net = require('net');
async.series([
function(next) {
fs.exists(socketPath, function(exists) {
if (exists) {
next();
} else {
callback();
}
});
},
function(next) {
var testSocket = new net.Socket();
testSocket.on('error', function(err) {
next(err.code !== 'ECONNREFUSED' ? err : null);
});
testSocket.connect({ path: socketPath }, function() {
// Something's listening here, abort
callback(new Error('port-in-use'));
});
},
async.apply(fs.unlink, socketPath), // The socket was stale, kick it out of the way
], callback);
};
'use strict';
var path = require('path'),
fs = require('fs'),
nconf = require('nconf'),
express = require('express'),
app = express(),
server,
winston = require('winston'),
async = require('async'),
emailer = require('./emailer'),
meta = require('./meta'),
logger = require('./logger'),
plugins = require('./plugins'),
middleware = require('./middleware'),
routes = require('./routes'),
emitter = require('./emitter'),
helpers = require('../public/src/modules/helpers');
if (nconf.get('ssl')) {
server = require('https').createServer({
key: fs.readFileSync(nconf.get('ssl').key),
cert: fs.readFileSync(nconf.get('ssl').cert)
}, app);
} else {
server = require('http').createServer(app);
}
module.exports.server = server;
server.on('error', function(err) {
winston.error(err);
if (err.code === 'EADDRINUSE') {
winston.error('NodeBB address in use, exiting...');
process.exit(0);
} else {
throw err;
}
});
module.exports.listen = function() {
emailer.registerApp(app);
middleware = middleware(app);
helpers.register();
logger.init(app);
emitter.all(['templates:compiled', 'meta:js.compiled', 'meta:css.compiled'], function() {
winston.info('NodeBB Ready');
emitter.emit('nodebb:ready');
listen();
});
initializeNodeBB(function(err) {
if (err) {
winston.error(err);
process.exit();
}
if (process.send) {
process.send({
action: 'ready'
});
}
});
};
function initializeNodeBB(callback) {
var skipJS, skipLess, fromFile = nconf.get('from-file') || '';
if (fromFile.match('js')) {
winston.info('[minifier] Minifying client-side JS skipped');
skipJS = true;
}
if (fromFile.match('less')) {
winston.info('[minifier] Compiling LESS files skipped');
skipLess = true;
}
async.waterfall([
async.apply(cacheStaticFiles),
async.apply(meta.themes.setupPaths),
function(next) {
plugins.init(app, middleware, next);
},
function(next) {
async.parallel([
async.apply(meta.templates.compile),
async.apply(!skipJS ? meta.js.minify : meta.js.getFromFile, app.enabled('minification')),
async.apply(!skipLess ? meta.css.minify : meta.css.getFromFile),
async.apply(meta.sounds.init)
], next);
},
function(results, next) {
plugins.fireHook('static:app.preload', {
app: app,
middleware: middleware
}, next);
},
function(next) {
routes(app, middleware);
next();
}
], callback);
}
function cacheStaticFiles(callback) {
if (global.env === 'development') {
return callback();
}
app.enable('cache');
app.enable('minification');
// Configure cache-buster timestamp
require('child_process').exec('git describe --tags', {
cwd: path.join(__dirname, '../')
}, function(err, stdOut) {
if (!err) {
meta.config['cache-buster'] = stdOut.trim();
callback();
} else {
fs.stat(path.join(__dirname, '../package.json'), function(err, stats) {
if (err) {
return callback(err);
}
meta.config['cache-buster'] = new Date(stats.mtime).getTime();
callback();
});
}
});
}
function listen(callback) {
var port = nconf.get('port');
if (Array.isArray(port)) {
if (!port.length) {
winston.error('[startup] empty ports array in config.json');
process.exit();
}
winston.warn('[startup] If you want to start nodebb on multiple ports please use loader.js');
winston.warn('[startup] Defaulting to first port in array, ' + port[0]);
port = port[0];
if (!port) {
winston.error('[startup] Invalid port, exiting');
process.exit();
}
}
if (port !== 80 && port !== 443 && nconf.get('use_port') === false) {
winston.info('Enabling \'trust proxy\'');
app.enable('trust proxy');
}
if ((port === 80 || port === 443) && process.env.NODE_ENV !== 'development') {
winston.info('Using ports 80 and 443 is not recommend; use a proxy instead. See README.md');
}
var isSocket = isNaN(port),
args = isSocket ? [port] : [port, nconf.get('bind_address')],
bind_address = ((nconf.get('bind_address') === "0.0.0.0" || !nconf.get('bind_address')) ? '0.0.0.0' : nconf.get('bind_address')) + ':' + port,
oldUmask;
args.push(function(err) {
if (err) {
winston.info('[startup] NodeBB was unable to listen on: ' + bind_address);
process.exit();
}
winston.info('NodeBB is now listening on: ' + (isSocket ? port : bind_address));
if (oldUmask) {
process.umask(oldUmask);
}
});
// Alter umask if necessary
if (isSocket) {
oldUmask = process.umask('0000');
module.exports.testSocket(port, function(err) {
if (!err) {
server.listen.apply(server, args);
} else {
winston.error('[startup] NodeBB was unable to secure domain socket access (' + port + ')');
winston.error('[startup] ' + err.message);
process.exit();
}
});
} else {
server.listen.apply(server, args);
}
}
module.exports.testSocket = function(socketPath, callback) {
if (typeof socketPath !== 'string') {
return callback(new Error('invalid socket path : ' + socketPath));
}
var net = require('net');
async.series([
function(next) {
fs.exists(socketPath, function(exists) {
if (exists) {
next();
} else {
callback();
}
});
},
function(next) {
var testSocket = new net.Socket();
testSocket.on('error', function(err) {
next(err.code !== 'ECONNREFUSED' ? err : null);
});
testSocket.connect({ path: socketPath }, function() {
// Something's listening here, abort
callback(new Error('port-in-use'));
});
},
async.apply(fs.unlink, socketPath), // The socket was stale, kick it out of the way
], callback);
};

Loading…
Cancel
Save