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