Merge pull request #432 from aarontc/development
[KiwiIRC.git] / server / httphandler.js
CommitLineData
bd299b17 1var url = require('url'),
0fa2ca42 2 fs = require('fs'),
cbcd1a23 3 crypto = require('crypto'),
0fa2ca42 4 node_static = require('node-static'),
cbcd1a23
JA
5 _ = require('lodash'),
6 config = require('./configuration.js');
a8bf3ea4 7
a8bf3ea4 8
3eb6b5fd
D
9// Cache for settings.json
10var cached_settings = {
11 debug: {
12 hash: '',
13 settings: ''
14 },
15 production: {
16 hash: '',
17 settings: ''
18 }
19};
20
21// Clear the settings cache when the settings change
22config.on('loaded', function () {
23 cached_settings.debug.settings = cached_settings.production.settings = '';
24 cached_settings.debug.hash = cached_settings.production.hash = '';
25});
26
27
28
a8bf3ea4 29
186531ed 30var HttpHandler = function (config) {
ceb43091
D
31 var public_http = config.public_http || 'client/';
32 this.file_server = new node_static.Server(public_http);
a8bf3ea4
JA
33};
34
186531ed
D
35module.exports.HttpHandler = HttpHandler;
36
37
38
39HttpHandler.prototype.serve = function (request, response) {
b65ad8f1 40 // The incoming requests base path (ie. /kiwiclient)
b737610b 41 var base_path = global.config.http_base_path || '/kiwi',
b65ad8f1
D
42 base_path_regex;
43
44 // Trim of any trailing slashes
45 if (base_path.substr(base_path.length - 1) === '/') {
46 base_path = base_path.substr(0, base_path.length - 1);
47 }
0fa2ca42 48
b65ad8f1
D
49 // Build the regex to match the base_path
50 base_path_regex = base_path.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
0ae87edb
D
51
52 // Any asset request to head into the asset dir
b65ad8f1 53 request.url = request.url.replace(base_path + '/assets/', '/assets/');
0ae87edb 54
c3511215
JA
55 // Any src request to head into the src dir
56 request.url = request.url.replace(base_path + '/src/', '/src/');
57
b075a0d6 58 // Any requests for /client to load the index file
b65ad8f1 59 if (request.url.match(new RegExp('^' + base_path_regex + '([/$]|$)', 'i'))) {
92f1ff73 60 request.url = '/index.html';
b075a0d6
D
61 }
62
0fa2ca42 63 // If the 'magic' translation is requested, figure out the best language to use from
b722f3c6 64 // the Accept-Language HTTP header. If nothing is suitible, fallback to our en-gb default translation
28cde487
JA
65 if (request.url.substr(0, 16) === '/assets/locales/') {
66 if (request.url === '/assets/locales/magic.json') {
67 return serveMagicLocale.call(this, request, response);
68 } else {
69 response.setHeader('Content-Language', request.url.substr(16, request.url.indexOf('.') - 16));
70 }
cbcd1a23
JA
71 } else if (request.url.substr(0, 21) === '/assets/settings.json') {
72 return serveSettings.call(this, request, response);
0fa2ca42 73 }
0ae87edb 74
186531ed 75 this.file_server.serve(request, response, function (err) {
a8bf3ea4 76 if (err) {
bd299b17
JA
77 response.writeHead(err.status, err.headers);
78 response.end();
a8bf3ea4
JA
79 }
80 });
0fa2ca42
JA
81};
82
3eb6b5fd
D
83
84/**
85 * Handle the /assets/locales/magic.json request
86 * Find the closest translation we have for the language
87 * set in the browser.
88 **/
0fa2ca42 89var serveMagicLocale = function (request, response) {
3eb6b5fd
D
90 var that = this,
91 default_locale_id = 'en-gb';
8e1ab29d 92
0fa2ca42 93 if (request.headers['accept-language']) {
28cde487
JA
94 fs.readdir('client/assets/locales', function (err, files) {
95 var available = [],
96 i = 0,
3eb6b5fd
D
97 langs = request.headers['accept-language'].split(','), // Example: en-gb,en;q=0.5
98 found_locale = default_locale_id;
28cde487 99
8e1ab29d 100 // Get a list of the available translations we have
0fa2ca42 101 files.forEach(function (file) {
28cde487
JA
102 if (file.substr(-5) === '.json') {
103 available.push(file.slice(0, -5));
0fa2ca42
JA
104 }
105 });
0fa2ca42 106
8e1ab29d 107 // Sanitise the browsers accepted languages and the qualities
28cde487
JA
108 for (i = 0; i < langs.length; i++) {
109 langs[i]= langs[i].split(';q=');
8e1ab29d 110 langs[i][0] = langs[i][0].toLowerCase();
28cde487 111 langs[i][1] = (typeof langs[i][1] === 'string') ? parseFloat(langs[i][1]) : 1.0;
0fa2ca42 112 }
8e1ab29d
D
113
114 // Sort the accepted languages by quality
28cde487
JA
115 langs.sort(function (a, b) {
116 return b[1] - a[1];
117 });
8e1ab29d
D
118
119 // Serve the first language we have a translation for
28cde487
JA
120 for (i = 0; i < langs.length; i++) {
121 if (langs[i][0] === '*') {
122 break;
123 } else if (_.contains(available, langs[i][0])) {
3eb6b5fd
D
124 found_locale = langs[i][0];
125 break;
28cde487
JA
126 }
127 }
8e1ab29d 128
3eb6b5fd 129 return serveLocale.call(that, request, response, found_locale);
28cde487 130 });
d2831ddd 131 } else {
3eb6b5fd
D
132
133 // No accept-language specified in the request so send the default
134 return serveLocale.call(this, request, response, default_locale_id);
0fa2ca42 135 }
3eb6b5fd 136
d2831ddd 137};
cbc8feae 138
3eb6b5fd
D
139
140/**
141 * Send a locale to the browser
142 */
143var serveLocale = function (request, response, locale_id) {
144 this.file_server.serveFile('/assets/locales/' + locale_id + '.json', 200, {
145 Vary: 'Accept-Language',
146 'Content-Language': locale_id
147 }, request, response);
0fa2ca42 148};
cbcd1a23 149
3eb6b5fd
D
150
151/**
152 * Handle the settings.json request
153 */
154function serveSettings(request, response) {
155 var referrer_url,
156 debug = false,
157 settings;
158
159 // Check the referrer for a debug option
160 if (request.headers['referer']) {
161 referrer_url = url.parse(request.headers['referer'], true);
162 if (referrer_url.query && referrer_url.query.debug) {
163 debug = true;
164 }
498afd32 165 }
cbe80532 166
3eb6b5fd
D
167 settings = cached_settings[debug ? 'debug' : 'production'];
168
169 // Generate the settings if we don't have them cached as yet
170 if (settings.settings === '') {
171 generateSettings(request, debug, function (err, settings) {
172 if (err) {
173 response.statusCode = 500;
174 response.end();
175 } else {
176 sendSettings.call(this, request, response, settings);
177 }
178 });
cbe80532 179
3eb6b5fd
D
180 } else {
181 sendSettings.call(this, request, response, settings);
182 }
183}
184
185
186/**
187 * Send the the settings to the browser
188 */
189function sendSettings(request, response, settings) {
190 if (request.headers['if-none-match'] && request.headers['if-none-match'] === settings.hash) {
191 response.writeHead(304, 'Not Modified');
192 return response.end();
193 }
194
195 response.writeHead(200, {
196 'ETag': settings.hash,
197 'Content-Type': 'application/json'
198 });
199 response.end(settings.settings);
200}
201
202
203/**
204 * Generate a settings object for the client.
205 * Settings include available translations, default client config, etc
206 */
498afd32 207function generateSettings(request, debug, callback) {
cbcd1a23
JA
208 var vars = {
209 server_settings: {},
210 client_plugins: [],
211 translations: [],
212 scripts: [
213 [
c3511215 214 'assets/libs/lodash.min.js'
cbcd1a23 215 ],
c3511215 216 ['assets/libs/backbone.min.js', 'assets/libs/jed.js']
cbcd1a23 217 ]
498afd32 218 };
cbcd1a23 219
498afd32 220 if (debug) {
cbcd1a23 221 vars.scripts = vars.scripts.concat([
33432128
D
222 [
223 'src/app.js',
c3511215
JA
224 'assets/libs/engine.io.js',
225 'assets/libs/engine.io.tools.js'
33432128 226 ],
cbcd1a23
JA
227 [
228 'src/models/application.js',
229 'src/models/gateway.js'
230 ],
231 [
232 'src/models/newconnection.js',
233 'src/models/panellist.js',
234 'src/models/networkpanellist.js',
235 'src/models/panel.js',
236 'src/models/member.js',
237 'src/models/memberlist.js',
238 'src/models/network.js'
239 ],
d99e7823 240
cbcd1a23 241 [
cbcd1a23 242 'src/models/channel.js',
cbcd1a23
JA
243 'src/models/applet.js'
244 ],
245
246 [
a0c22d9e 247 'src/models/query.js',
f8508c7a 248 'src/models/server.js', // Depends on models/channel.js
cbcd1a23
JA
249 'src/applets/settings.js',
250 'src/applets/chanlist.js',
251 'src/applets/scripteditor.js'
252 ],
253
254 [
255 'src/models/pluginmanager.js',
256 'src/models/datastore.js',
257 'src/helpers/utils.js'
258 ],
259
260 // Some views extend these, so make sure they're loaded beforehand
261 [
262 'src/views/panel.js'
263 ],
264
265 [
266 'src/views/channel.js',
267 'src/views/applet.js',
268 'src/views/application.js',
269 'src/views/apptoolbar.js',
270 'src/views/controlbox.js',
271 'src/views/favicon.js',
272 'src/views/mediamessage.js',
273 'src/views/member.js',
274 'src/views/memberlist.js',
275 'src/views/menubox.js',
276 'src/views/networktabs.js',
277 'src/views/nickchangebox.js',
278 'src/views/resizehandler.js',
279 'src/views/serverselect.js',
280 'src/views/statusmessage.js',
281 'src/views/tabs.js',
282 'src/views/topicbar.js',
283 'src/views/userbox.js'
284 ]
285 ]);
286 } else {
c3511215 287 vars.scripts.push(['assets/kiwi.min.js', 'assets/libs/engine.io.bundle.min.js']);
cbcd1a23
JA
288 }
289
290 // Any restricted server mode set?
291 if (config.get().restrict_server) {
292 vars.server_settings = {
293 connection: {
294 server: config.get().restrict_server,
295 port: config.get().restrict_server_port || 6667,
296 ssl: config.get().restrict_server_ssl,
297 channel: config.get().restrict_server_channel,
298 nick: config.get().restrict_server_nick,
299 allow_change: false
300 }
301 };
302 }
303
304 // Any client default settings?
305 if (config.get().client) {
306 vars.server_settings.client = config.get().client;
307 }
308
309 // Any client plugins?
310 if (config.get().client_plugins && config.get().client_plugins.length > 0) {
311 vars.client_plugins = config.get().client_plugins;
312 }
313
3eb6b5fd 314 // Get a list of available translations
c3511215 315 fs.readFile(__dirname + '/../client/src/translations/translations.json', function (err, translations) {
cbcd1a23 316 if (err) {
cbe80532 317 return callback(err);
cbcd1a23
JA
318 }
319
320 var translation_files;
321 translations = JSON.parse(translations);
c3511215 322 fs.readdir(__dirname + '/../client/src/translations/', function (err, pofiles) {
498afd32 323 var hash, settings;
cbcd1a23 324 if (err) {
cbe80532 325 return callback(err);
cbcd1a23
JA
326 }
327
328 pofiles.forEach(function (file) {
329 var locale = file.slice(0, -3);
330 if ((file.slice(-3) === '.po') && (locale !== 'template')) {
331 vars.translations.push({tag: locale, language: translations[locale]});
332 }
333 });
334
498afd32
JA
335 settings = cached_settings[debug?'debug':'production'];
336 settings.settings = JSON.stringify(vars);
342de126 337 settings.hash = crypto.createHash('md5').update(settings.settings).digest('hex');
cbcd1a23 338
498afd32 339 return callback(null, settings);
cbe80532
JA
340 });
341 });
342}
cbcd1a23 343