fossasia/loklak_webclient

View on GitHub
iframely/utils.js

Summary

Maintainability
C
1 day
Test Coverage
(function() {

    GLOBAL.CONFIG = require('./config');

    var async = require('async');
    var cache = require('./lib/cache');
    var ejs = require('ejs');
    var fs = require('fs');
    var crypto = require('crypto');
    var moment = require('moment');
    var _ = require('underscore');
    var urlLib = require('url');

    var log = exports.log = require('./logging').log;

    var whitelist = require('./lib/whitelist');
    var pluginLoader = require('./lib/loader/pluginLoader');

    function NotFound(message) {

        if (typeof message === 'object') {
            this.meta = message;
            message = JSON.stringify(message, null, 4);
        }

        Error.call(this); //super constructor
        Error.captureStackTrace(this, this.constructor); //super helper method to include stack trace in error object

        this.name = this.constructor.name; //set our function’s name as error name.
        this.message = message; //set the error message

    }

    NotFound.prototype.__proto__ = Error.prototype;

    exports.NotFound = NotFound;

    function HttpError(code, message) {

        Error.call(this); //super constructor
        Error.captureStackTrace(this, this.constructor); //super helper method to include stack trace in error object

        this.name = this.constructor.name; //set our function’s name as error name.
        this.message = message; //set the error message
        this.code = code; //set the error code

    }

    HttpError.prototype.__proto__ = Error.prototype;

    exports.HttpError = HttpError;

    var send = require('send')
        , utils = require('connect/lib/utils')
        , parse = utils.parseUrl
        , url = require('url');


    exports.static = function(root, options){
        options = options || {};

        // root required
        if (!root) throw new Error('static() root path required');

        // default redirect
        var redirect = false !== options.redirect;

        return function static(req, res, next) {
            if ('GET' != req.method && 'HEAD' != req.method) return next();
            var path = parse(options.path ? {url: options.path} : req).pathname;
            var pause = utils.pause(req);

            function resume() {
                next();
                pause.resume();
            }

            function directory() {
                if (!redirect) return resume();
                var pathname = url.parse(req.originalUrl).pathname;
                res.statusCode = 301;
                res.setHeader('Location', pathname + '/');
                res.end('Redirecting to ' + utils.escape(pathname) + '/');
            }

            function error(err) {
                if (404 == err.status) return resume();
                next(err);
            }

            send(req, path)
                .maxage(options.maxAge || 0)
                .root(root)
                .hidden(options.hidden)
                .on('error', error)
                .on('directory', directory)
                .pipe(res);
        };
    };

    var version = require('./package.json').version;

    var etag = function(value) {
        return '"' + crypto.createHash('md5').update(value).digest("hex") + '"';
    };

    function prepareUri(uri) {

        if (!uri) {
            return uri;
        }

        if (uri.match(/^\/\//i)) {
            return "http:" + uri;
        }

        if (!uri.match(/^https?:\/\//i)) {
            return "http://" + uri;
        }

        return uri;
    }

    function getKeyForUri(uri) {

        if (!uri) {
            return;
        }

        var result = 0;

        var whitelistRecord = whitelist.findRawWhitelistRecordFor(uri);
        if (whitelistRecord) {
            result += new Date(whitelistRecord.date).getTime();
        }

        var plugin = pluginLoader.findDomainPlugin(uri);
        if (plugin) {
            result += plugin.getPluginLastModifiedDate().getTime();
        }

        if (result) {
            result = Math.round(result / 1000);
        }

        return result || null;
    }

    function getUnifiedCacheUrl(req) {

        // Remove 'refresh' param and order keys.

        var urlObj = urlLib.parse(req.url, true);

        var query = urlObj.query;

        delete query.refresh;

        // Remove jsonp params.
        // TODO: remove all except possible params.
        delete query._;
        delete query[req.app.get('jsonp callback name')];
        delete query.fingerprint;
        delete query.lang;
        delete query.access_token;

        delete urlObj.search;

        var newQuery = {};

        var keys = _.keys(query);
        keys.sort();
        keys.forEach(function(key) {
            newQuery[key] = query[key];
        });

        urlObj.query = newQuery;

        return urlLib.format(urlObj);
    }

    function setResponseToCache(code, content_type, req, res, body, ttl) {

        if (!res.get('ETag')) {
            res.set('ETag', etag(body));
        }

        var url = getUnifiedCacheUrl(req);

        var head = {
            statusCode: code,
            headers: {
                'Content-Type': content_type
            },
            etag: res.get('ETag')
        };

        var data = JSON.stringify(head) + '::' + body;

        var linkValidationKey, uri = prepareUri(req.query.uri || req.query.url);
        if (uri) {
            linkValidationKey = getKeyForUri(uri);
        }

        cache.set('urlcache:' + version + (linkValidationKey || '') + ':' + url, data, {ttl: ttl});
    }

    exports.cacheMiddleware = function(req, res, next) {

        async.waterfall([

            function(cb) {
                var refresh = req.query.refresh === "true" || req.query.refresh === "1";
                if (!refresh) {

                    var url = getUnifiedCacheUrl(req);

                    var linkValidationKey, uri = prepareUri(req.query.uri || req.query.url);
                    if (uri) {
                        linkValidationKey = getKeyForUri(uri);
                    }

                    cache.get('urlcache:' + version + (linkValidationKey || '') + ':' + url, function(error, data) {
                        if (error) {
                            console.error('Error getting response from cache', url, error);
                        }
                        if (data) {
                            var index = data.indexOf("::");
                            if (index > -1) {
                                var head;
                                var headStr = data.substring(0, index);
                                try {
                                    head = JSON.parse(headStr);
                                } catch(ex) {
                                    console.error('Error parsing response status from cache', url, headStr);
                                }

                                if (head) {

                                    log(req, "Using cache for", req.url.replace(/\?.+/, ''), req.query.uri || req.query.url);

                                    var requestedEtag = req.headers['if-none-match'];

                                    var jsonpCallback = req.query[req.app.get('jsonp callback name')];
                                    if (jsonpCallback) {

                                        // jsonp case.

                                        var body = data.substring(index + 2);

                                        body = body
                                            .replace(/\u2028/g, '\\u2028')
                                            .replace(/\u2029/g, '\\u2029');

                                        jsonpCallback = jsonpCallback.replace(/[^\[\]\w$.]/g, '');
                                        body = jsonpCallback + ' && ' + jsonpCallback + '(' + body + ');';

                                        var realEtag = etag(body);

                                        if (realEtag === requestedEtag) {
                                            res.writeHead(304);
                                            res.end();
                                        } else {
                                            this.charset = this.charset || 'utf-8';
                                            res.set('ETag', realEtag);
                                            res.set('Content-Type', 'text/javascript');
                                            res.writeHead(head.statusCode || 200, head.headers);
                                            res.end(body);
                                        }

                                    } else {

                                        // Common case.

                                        if (head.etag === requestedEtag) {
                                            res.writeHead(304);
                                            res.end();
                                        } else {
                                            this.charset = this.charset || 'utf-8';
                                            if (head.etag) {
                                                res.set('ETag', head.etag);
                                            }
                                            res.writeHead(head.statusCode || 200, head.headers);
                                            res.end(data.substring(index + 2));
                                        }
                                    }

                                } else {
                                    cb();
                                }
                            }
                        } else {
                            cb();
                        }
                    });

                } else {
                    cb();
                }
            }

        ], function() {

            // Copy from source.
            res.renderCached = function(view, context, headers) {

                if (!fs.existsSync(view)) {
                    view = __dirname + '/views/' + view;
                }

                var template = fs.readFileSync(view, 'utf8');
                var body = ejs.render(template, context);

                setResponseToCache(200, 'text/html', req, res, body);

                this.charset = this.charset || 'utf-8';
                this.writeHead(200, headers);
                this.end(body);
            };

            // Copy from source.
            res.jsonpCached = function(obj) {

                // settings
                var app = this.app;
                var replacer = app.get('json replacer');
                var spaces = app.get('json spaces');
                var body = JSON.stringify(obj, replacer, spaces);

                // content-type
                this.charset = this.charset || 'utf-8';
                this.set('Content-Type', 'application/json');

                // Cache without jsonp callback.
                setResponseToCache(200, 'application/json', req, res, body);

                // jsonp
                var callback = this.req.query[app.get('jsonp callback name')];
                if (callback) {
                    body = body
                        .replace(/\u2028/g, '\\u2028')
                        .replace(/\u2029/g, '\\u2029');

                    this.set('Content-Type', 'text/javascript');
                    var cb = callback.replace(/[^\[\]\w$.]/g, '');
                    body = cb + ' && ' + cb + '(' + body + ');';
                }

                this.send(body);
            };
            res.tryCacheError = function(error) {

                if (typeof error === "number" && Math.floor(error / 100) === 4) {

                    var value;

                    if (error === 404) {
                        value = 'Page not found';
                    } else {
                        value = 'Requested page error: ' + error;
                    }

                    setResponseToCache(error, 'text/html', req, res, value, CONFIG.CACHE_TTL_PAGE_404);

                } else if (typeof error === "string" && error.match(/^timeout/)) {

                    setResponseToCache(408, 'text/html', req, res, 'Requested page error: ' + error, CONFIG.CACHE_TTL_PAGE_TIMEOUT);
                }
            };

            res.sendCached = function(content_type, body) {

                setResponseToCache(200, content_type, req, res, body);

                this.charset = this.charset || 'utf-8';
                this.writeHead(200, {'Content-Type': content_type});
                this.end(body);
            };

            res.sendJsonCached = function(obj) {

                var app = this.app;
                var replacer = app.get('json replacer');
                var spaces = app.get('json spaces');

                var body = JSON.stringify(obj, replacer, spaces);

                // content-type
                this.charset = this.charset || 'utf-8';
                this.set('Content-Type', 'application/json');

                setResponseToCache(200, 'application/json', req, res, body);

                this.send(body);
            };


            next();
        });
    };

})();