<commit_before>odoo.define('web.WebClient', function (require) {
"use strict";

var AbstractWebClient = require('web.AbstractWebClient');
var config = require('web.config');
var data_manager = require('web.data_manager');
var dom = require('web.dom');
var framework = require('web.framework');
var Menu = require('web.Menu');
var session = require('web.session');
var SystrayMenu = require('web.SystrayMenu');
var UserMenu = require('web.UserMenu');

return AbstractWebClient.extend({
    events: {
        'click .oe_logo_edit_admin': 'logo_edit',
        'click .oe_logo img': function(ev) {
            ev.preventDefault();
            return this.clear_uncommitted_changes().then(function() {
                framework.redirect("/web" + (config.debug ? "?debug" : ""));
            });
        },
    },
    show_application: function() {
        var self = this;

        this.toggle_bars(true);
        this.set_title();
        this.update_logo();

        // Menu is rendered server-side thus we don't want the widget to create any dom
        this.menu = new Menu(this);
        this.menu.setElement(this.$el.parents().find('.oe_application_menu_placeholder'));
        this.menu.on('menu_click', this, this.on_menu_action);

        // Create the user menu (rendered client-side)
        this.user_menu = new UserMenu(this);
        var $user_menu_placeholder = $('body').find('.oe_user_menu_placeholder').show();
        var user_menu_loaded = this.user_menu.appendTo($user_menu_placeholder);

        // Create the systray menu (rendered server-side)
        this.systray_menu = new SystrayMenu(this);
        this.systray_menu.setElement(this.$el.parents().find('.oe_systray'));
        var systray_menu_loaded = this.systray_menu.start();

        // Start the menu once both systray and user menus are rendered
        // to prevent overflows while loading
        return $.when(systray_menu_loaded, user_menu_loaded).then(function() {
            self.menu.start();
            self.bind_hashchange();
        });

    },
    toggle_bars: function(value) {
        this.$('tr:has(td.navbar),.oe_leftbar').toggle(value);
    },
    update_logo: function(reload) {
        var company = session.company_id;
        var img = session.url('/web/binary/company_logo' + '?db=' + session.db + (company ? '&company=' + company : ''));
        this.$('.o_sub_menu_logo img').attr('src', '').attr('src', img + (reload ? "&t=" + Date.now() : ''));
        this.$('.oe_logo_edit').toggleClass('oe_logo_edit_admin', session.is_superuser);
    },
    logo_edit: function(ev) {
        var self = this;
        ev.preventDefault();
        this._rpc({
                model: 'res.users',
                method: 'read',
                args: [[session.uid], ['company_id']],
            })
            .then(function(data) {
                self._rpc({
                        route: '/web/action/load',
                        params: { action_id: 'base.action_res_company_form' },
                    })
                    .done(function(result) {
                        result.res_id = data[0].company_id[0];
                        result.target = "new";
                        result.views = [[false, 'form']];
                        result.flags = {
                            action_buttons: true,
                            headless: true,
                        };
                        self.action_manager.doAction(result, {
                            on_close: self.update_logo.bind(self, true),
                        });
                    });
            });
        return false;
    },
    bind_hashchange: function() {
        var self = this;
        $(window).bind('hashchange', this.on_hashchange);

        var state = $.bbq.getState(true);
        if (_.isEmpty(state) || state.action === "login") {
            self.menu.is_bound.done(function() {
                self._rpc({
                        model: 'res.users',
                        method: 'read',
                        args: [[session.uid], ['action_id']],
                    })
                    .done(function(result) {
                        var data = result[0];
                        if(data.action_id) {
                            self.action_manager.doAction(data.action_id[0]);
                            self.menu.open_action(data.action_id[0]);
                        } else {
                            var first_menu_id = self.menu.$el.find("a:first").data("menu");
                            if(first_menu_id) {
                                self.menu.menu_click(first_menu_id);
                            }
                        }
                    });
            });
        } else {
            $(window).trigger('hashchange');
        }
    },
    on_hashchange: function(event) {
        if (this._ignore_hashchange) {
            this._ignore_hashchange = false;
            return;
        }

        var self = this;
        this.clear_uncommitted_changes().then(function () {
            var stringstate = event.getState(false);
            if (!_.isEqual(self._current_state, stringstate)) {
                var state = event.getState(true);
                if(!state.action && state.menu_id) {
                    self.menu.is_bound.done(function() {
                        self.menu.menu_click(state.menu_id);
                    });
                } else {
                    self.action_manager.loadState(state, !!self._current_state).then(function () {
                        var action = self.action_manager.getCurrentAction();
                        if (action) {
                            self.menu.open_action(action.id, state.menu_id);
                        }
                    });
                }
            }
            self._current_state = stringstate;
        }, function () {
            if (event) {
                self._ignore_hashchange = true;
                window.location = event.originalEvent.oldURL;
            }
        });
    },
    on_menu_action: function(options) {
        var self = this;
        return this.menu_dm.add(data_manager.load_action(options.action_id))
            .then(function (result) {
                return self.action_mutex.exec(function() {
                    var completed = $.Deferred();
                    self.action_manager.doAction(result, {
                        clear_breadcrumbs: true,
                        action_menu_id: options.id,
                    }).fail(function() {
                        self.menu.open_menu(options.previous_menu_id);
                    }).always(function() {
                        completed.resolve();
                    });
                    setTimeout(function() {
                        completed.resolve();
                    }, 2000);
                    // We block the menu when clicking on an element until the action has correctly finished
                    // loading. If something crash, there is a 2 seconds timeout before it's unblocked.
                    return completed;
                });
            });
    },
    toggle_fullscreen: function(fullscreen) {
        this._super(fullscreen);
        if (!fullscreen) {
            this.menu.reflow();
        }
    },

    //--------------------------------------------------------------------------
    // Handlers
    //--------------------------------------------------------------------------

    /**
     * @override
     */
    _onGetScrollPosition: function (ev) {
        ev.data.callback({
            left: this.action_manager.el.scrollLeft,
            top: this.action_manager.el.scrollTop,
        });
    },
    /**
     * @override
     */
    _onScrollTo: function (ev) {
        var offset;
        if (ev.data.selector) {
            offset = dom.getPosition(document.querySelector(ev.data.selector));
            // substract the position of the ActionManager as it is the
            // scrolling element
            var actionManagerOffset = dom.getPosition(this.action_manager.el);
            offset.left -= actionManagerOffset.left;
            offset.top -= actionManagerOffset.top;
        } else {
            offset = {top: ev.data.top || 0, left: ev.data.left || 0};
        }

        this.action_manager.el.scrollTop = offset.top;
        this.action_manager.el.scrollLeft = offset.left;
    },
});

});
<commit_msg>[FIX] web: fix issue with initial action in web client<commit_after>odoo.define('web.WebClient', function (require) {
"use strict";

var AbstractWebClient = require('web.AbstractWebClient');
var config = require('web.config');
var data_manager = require('web.data_manager');
var dom = require('web.dom');
var framework = require('web.framework');
var Menu = require('web.Menu');
var session = require('web.session');
var SystrayMenu = require('web.SystrayMenu');
var UserMenu = require('web.UserMenu');

return AbstractWebClient.extend({
    events: {
        'click .oe_logo_edit_admin': 'logo_edit',
        'click .oe_logo img': function(ev) {
            ev.preventDefault();
            return this.clear_uncommitted_changes().then(function() {
                framework.redirect("/web" + (config.debug ? "?debug" : ""));
            });
        },
    },
    show_application: function() {
        var self = this;

        this.toggle_bars(true);
        this.set_title();
        this.update_logo();

        // Menu is rendered server-side thus we don't want the widget to create any dom
        this.menu = new Menu(this);
        this.menu.setElement(this.$el.parents().find('.oe_application_menu_placeholder'));
        this.menu.on('menu_click', this, this.on_menu_action);

        // Create the user menu (rendered client-side)
        this.user_menu = new UserMenu(this);
        var $user_menu_placeholder = $('body').find('.oe_user_menu_placeholder').show();
        var user_menu_loaded = this.user_menu.appendTo($user_menu_placeholder);

        // Create the systray menu (rendered server-side)
        this.systray_menu = new SystrayMenu(this);
        this.systray_menu.setElement(this.$el.parents().find('.oe_systray'));
        var systray_menu_loaded = this.systray_menu.start();

        // Start the menu once both systray and user menus are rendered
        // to prevent overflows while loading
        return $.when(systray_menu_loaded, user_menu_loaded).then(function() {
            self.menu.start();
            self.bind_hashchange();
        });

    },
    toggle_bars: function(value) {
        this.$('tr:has(td.navbar),.oe_leftbar').toggle(value);
    },
    update_logo: function(reload) {
        var company = session.company_id;
        var img = session.url('/web/binary/company_logo' + '?db=' + session.db + (company ? '&company=' + company : ''));
        this.$('.o_sub_menu_logo img').attr('src', '').attr('src', img + (reload ? "&t=" + Date.now() : ''));
        this.$('.oe_logo_edit').toggleClass('oe_logo_edit_admin', session.is_superuser);
    },
    logo_edit: function(ev) {
        var self = this;
        ev.preventDefault();
        this._rpc({
                model: 'res.users',
                method: 'read',
                args: [[session.uid], ['company_id']],
            })
            .then(function(data) {
                self._rpc({
                        route: '/web/action/load',
                        params: { action_id: 'base.action_res_company_form' },
                    })
                    .done(function(result) {
                        result.res_id = data[0].company_id[0];
                        result.target = "new";
                        result.views = [[false, 'form']];
                        result.flags = {
                            action_buttons: true,
                            headless: true,
                        };
                        self.action_manager.doAction(result, {
                            on_close: self.update_logo.bind(self, true),
                        });
                    });
            });
        return false;
    },
    bind_hashchange: function() {
        var self = this;
        $(window).bind('hashchange', this.on_hashchange);
        var didHashChanged = false;
        $(window).one('hashchange', function () {
            didHashChanged = true;
        });

        var state = $.bbq.getState(true);
        if (_.isEmpty(state) || state.action === "login") {
            self.menu.is_bound.done(function() {
                self._rpc({
                        model: 'res.users',
                        method: 'read',
                        args: [[session.uid], ['action_id']],
                    })
                    .done(function(result) {
                        if (didHashChanged) {
                            return;
                        }
                        var data = result[0];
                        if(data.action_id) {
                            self.action_manager.doAction(data.action_id[0]);
                            self.menu.open_action(data.action_id[0]);
                        } else {
                            var first_menu_id = self.menu.$el.find("a:first").data("menu");
                            if(first_menu_id) {
                                self.menu.menu_click(first_menu_id);
                            }
                        }
                    });
            });
        } else {
            $(window).trigger('hashchange');
        }
    },
    on_hashchange: function(event) {
        if (this._ignore_hashchange) {
            this._ignore_hashchange = false;
            return;
        }

        var self = this;
        this.clear_uncommitted_changes().then(function () {
            var stringstate = event.getState(false);
            if (!_.isEqual(self._current_state, stringstate)) {
                var state = event.getState(true);
                if(!state.action && state.menu_id) {
                    self.menu.is_bound.done(function() {
                        self.menu.menu_click(state.menu_id);
                    });
                } else {
                    self.action_manager.loadState(state, !!self._current_state).then(function () {
                        var action = self.action_manager.getCurrentAction();
                        if (action) {
                            self.menu.open_action(action.id, state.menu_id);
                        }
                    });
                }
            }
            self._current_state = stringstate;
        }, function () {
            if (event) {
                self._ignore_hashchange = true;
                window.location = event.originalEvent.oldURL;
            }
        });
    },
    on_menu_action: function(options) {
        var self = this;
        return this.menu_dm.add(data_manager.load_action(options.action_id))
            .then(function (result) {
                return self.action_mutex.exec(function() {
                    var completed = $.Deferred();
                    self.action_manager.doAction(result, {
                        clear_breadcrumbs: true,
                        action_menu_id: options.id,
                    }).fail(function() {
                        self.menu.open_menu(options.previous_menu_id);
                    }).always(function() {
                        completed.resolve();
                    });
                    setTimeout(function() {
                        completed.resolve();
                    }, 2000);
                    // We block the menu when clicking on an element until the action has correctly finished
                    // loading. If something crash, there is a 2 seconds timeout before it's unblocked.
                    return completed;
                });
            });
    },
    toggle_fullscreen: function(fullscreen) {
        this._super(fullscreen);
        if (!fullscreen) {
            this.menu.reflow();
        }
    },

    //--------------------------------------------------------------------------
    // Handlers
    //--------------------------------------------------------------------------

    /**
     * @override
     */
    _onGetScrollPosition: function (ev) {
        ev.data.callback({
            left: this.action_manager.el.scrollLeft,
            top: this.action_manager.el.scrollTop,
        });
    },
    /**
     * @override
     */
    _onScrollTo: function (ev) {
        var offset;
        if (ev.data.selector) {
            offset = dom.getPosition(document.querySelector(ev.data.selector));
            // substract the position of the ActionManager as it is the
            // scrolling element
            var actionManagerOffset = dom.getPosition(this.action_manager.el);
            offset.left -= actionManagerOffset.left;
            offset.top -= actionManagerOffset.top;
        } else {
            offset = {top: ev.data.top || 0, left: ev.data.left || 0};
        }

        this.action_manager.el.scrollTop = offset.top;
        this.action_manager.el.scrollLeft = offset.left;
    },
});

});
<|endoftext|>


<commit_before>/*
    This file customizes the CKEditor to the requirements of Kajona.

    $Id$
*/

CKEDITOR.editorConfig = function(config) {
    /*
        CKEditor offers a lot of toolbar buttons, here's a full list:

        config.toolbar_standard =
            [
                ['Source','-','Save','NewPage','Preview','-','Templates'],
                ['Cut','Copy','Paste','PasteText','PasteFromWord','-','Print', 'SpellChecker', 'Scayt'],
                ['Undo','Redo','-','Find','Replace','-','SelectAll','RemoveFormat'],
                ['Form', 'Checkbox', 'Radio', 'TextField', 'Textarea', 'Select', 'Button', 'ImageButton', 'HiddenField'],
                '/',
                ['Bold','Italic','Underline','Strike','-','Subscript','Superscript'],
                ['NumberedList','BulletedList','-','Outdent','Indent','Blockquote'],
                ['JustifyLeft','JustifyCenter','JustifyRight','JustifyBlock'],
                ['Link','Unlink','Anchor'],
                ['Image','Flash','Table','HorizontalRule','Smiley','SpecialChar','PageBreak'],
                '/',
                ['Styles','Format','Font','FontSize'],
                ['TextColor','BGColor'],
                ['Maximize', 'ShowBlocks','-','About']
            ];
    */

    //default toolbar, e.g. used for the page paragraphs
    config.toolbar_standard =
        [
            ['Source','Save','-','Cut','Copy','Paste','PasteText','-'],
            ['Undo','Redo','-','RemoveFormat'],
            ['Link','Unlink','Anchor'],
            ['Image','Table','HorizontalRule','SpecialChar'],['Maximize'],
            '/',
            ['Font','FontSize'],
            ['Bold','Italic','Underline','Strike','Subscript','Superscript'],
            ['NumberedList','BulletedList','-','Outdent','Indent'],
            ['TextColor','BGColor']
        ];

    //limited toolbar, e.g. used for the downloads and the gallery
    config.toolbar_minimal =
        [
            ['Source','Save','-'],
            ['Undo','Redo','-','RemoveFormat'],
            ['Link','Unlink'],
            ['Bold','Italic','Underline'],
            ['NumberedList','BulletedList','-','Outdent','Indent']
        ];

    //limited toolbar but with support for images
    config.toolbar_minimalimage =
        [
            ['Source','Save','-'],
            ['Undo','Redo','-','RemoveFormat'],
            ['Link','Unlink', 'Image'],
            ['Bold','Italic','Underline'],
            ['NumberedList','BulletedList','-','Outdent','Indent']
        ];

    config.toolbar_pe_full =
        [
            ['Undo','Redo','-','RemoveFormat'],
            ['Bold','Italic','Underline','Strike','Subscript','Superscript'],
            ['NumberedList','BulletedList','-','Outdent','Indent'],
            ['Link','Unlink'],
            ['Image']
        ];

    config.toolbar_pe_lite =
        [
            ['Undo','Redo','-','RemoveFormat']
        ];

    //disable the conversion of special chars into html entities
    config.entities = false;
    config.entities_greek = false;
    config.entities_latin = false;

};<commit_msg>BUG: ckeditor no longer creates p-elements by default<commit_after>/*
    This file customizes the CKEditor to the requirements of Kajona.

    $Id$
*/

CKEDITOR.editorConfig = function(config) {
    /*
        CKEditor offers a lot of toolbar buttons, here's a full list:

        config.toolbar_standard =
            [
                ['Source','-','Save','NewPage','Preview','-','Templates'],
                ['Cut','Copy','Paste','PasteText','PasteFromWord','-','Print', 'SpellChecker', 'Scayt'],
                ['Undo','Redo','-','Find','Replace','-','SelectAll','RemoveFormat'],
                ['Form', 'Checkbox', 'Radio', 'TextField', 'Textarea', 'Select', 'Button', 'ImageButton', 'HiddenField'],
                '/',
                ['Bold','Italic','Underline','Strike','-','Subscript','Superscript'],
                ['NumberedList','BulletedList','-','Outdent','Indent','Blockquote'],
                ['JustifyLeft','JustifyCenter','JustifyRight','JustifyBlock'],
                ['Link','Unlink','Anchor'],
                ['Image','Flash','Table','HorizontalRule','Smiley','SpecialChar','PageBreak'],
                '/',
                ['Styles','Format','Font','FontSize'],
                ['TextColor','BGColor'],
                ['Maximize', 'ShowBlocks','-','About']
            ];
    */

    //default toolbar, e.g. used for the page paragraphs
    config.toolbar_standard =
        [
            ['Source','Save','-','Cut','Copy','Paste','PasteText','-'],
            ['Undo','Redo','-','RemoveFormat'],
            ['Link','Unlink','Anchor'],
            ['Image','Table','HorizontalRule','SpecialChar'],['Maximize'],
            '/',
            ['Font','FontSize'],
            ['Bold','Italic','Underline','Strike','Subscript','Superscript'],
            ['NumberedList','BulletedList','-','Outdent','Indent'],
            ['TextColor','BGColor']
        ];

    //limited toolbar, e.g. used for the downloads and the gallery
    config.toolbar_minimal =
        [
            ['Source','Save','-'],
            ['Undo','Redo','-','RemoveFormat'],
            ['Link','Unlink'],
            ['Bold','Italic','Underline'],
            ['NumberedList','BulletedList','-','Outdent','Indent']
        ];

    //limited toolbar but with support for images
    config.toolbar_minimalimage =
        [
            ['Source','Save','-'],
            ['Undo','Redo','-','RemoveFormat'],
            ['Link','Unlink', 'Image'],
            ['Bold','Italic','Underline'],
            ['NumberedList','BulletedList','-','Outdent','Indent']
        ];

    config.toolbar_pe_full =
        [
            ['Undo','Redo','-','RemoveFormat'],
            ['Bold','Italic','Underline','Strike','Subscript','Superscript'],
            ['NumberedList','BulletedList','-','Outdent','Indent'],
            ['Link','Unlink'],
            ['Image']
        ];

    config.toolbar_pe_lite =
        [
            ['Undo','Redo','-','RemoveFormat']
        ];

    //disable the conversion of special chars into html entities
    config.entities = false;
    config.entities_greek = false;
    config.entities_latin = false;
    config.autoParagraph = false;
};<|endoftext|>


<commit_before>odoo.define('web.WebClient', function (require) {
"use strict";

var AbstractWebClient = require('web.AbstractWebClient');
var config = require('web.config');
var data_manager = require('web.data_manager');
var dom = require('web.dom');
var framework = require('web.framework');
var Menu = require('web.Menu');
var session = require('web.session');
var SystrayMenu = require('web.SystrayMenu');
var UserMenu = require('web.UserMenu');

return AbstractWebClient.extend({
    events: _.extend({}, AbstractWebClient.prototype.events, {
        'click .oe_logo_edit_admin': 'logo_edit',
        'click .oe_logo img': function(ev) {
            ev.preventDefault();
            return this.clear_uncommitted_changes().then(function() {
                framework.redirect("/web" + (config.debug ? "?debug" : ""));
            });
        },
    }),
    show_application: function() {
        var self = this;

        this.toggle_bars(true);
        this.set_title();
        this.update_logo();

        // Menu is rendered server-side thus we don't want the widget to create any dom
        this.menu = new Menu(this);
        this.menu.setElement(this.$el.parents().find('.oe_application_menu_placeholder'));
        this.menu.on('menu_click', this, this.on_menu_action);

        // Create the user menu (rendered client-side)
        this.user_menu = new UserMenu(this);
        var $user_menu_placeholder = $('body').find('.oe_user_menu_placeholder').show();
        var user_menu_loaded = this.user_menu.appendTo($user_menu_placeholder);

        // Create the systray menu (rendered server-side)
        this.systray_menu = new SystrayMenu(this);
        this.systray_menu.setElement(this.$el.parents().find('.oe_systray'));
        var systray_menu_loaded = this.systray_menu.start();

        // Start the menu once both systray and user menus are rendered
        // to prevent overflows while loading
        return $.when(systray_menu_loaded, user_menu_loaded).then(function() {
            self.menu.start();
            self.bind_hashchange();
        });

    },
    toggle_bars: function(value) {
        this.$('tr:has(td.navbar),.oe_leftbar').toggle(value);
    },
    update_logo: function(reload) {
        var company = session.company_id;
        var img = session.url('/web/binary/company_logo' + '?db=' + session.db + (company ? '&company=' + company : ''));
        this.$('.o_sub_menu_logo img').attr('src', '').attr('src', img + (reload ? "&t=" + Date.now() : ''));
        this.$('.oe_logo_edit').toggleClass('oe_logo_edit_admin', session.is_superuser);
    },
    logo_edit: function(ev) {
        var self = this;
        ev.preventDefault();
        this._rpc({
                model: 'res.users',
                method: 'read',
                args: [[session.uid], ['company_id']],
            })
            .then(function(data) {
                self._rpc({
                        route: '/web/action/load',
                        params: { action_id: 'base.action_res_company_form' },
                    })
                    .done(function(result) {
                        result.res_id = data[0].company_id[0];
                        result.target = "new";
                        result.views = [[false, 'form']];
                        result.flags = {
                            action_buttons: true,
                            headless: true,
                        };
                        self.action_manager.doAction(result, {
                            on_close: self.update_logo.bind(self, true),
                        });
                    });
            });
        return false;
    },
    bind_hashchange: function() {
        var self = this;
        $(window).bind('hashchange', this.on_hashchange);
        var didHashChanged = false;
        $(window).one('hashchange', function () {
            didHashChanged = true;
        });

        var state = $.bbq.getState(true);
        if (_.isEmpty(state) || state.action === "login") {
            self.menu.is_bound.done(function() {
                self._rpc({
                        model: 'res.users',
                        method: 'read',
                        args: [[session.uid], ['action_id']],
                    })
                    .done(function(result) {
                        if (didHashChanged) {
                            return;
                        }
                        var data = result[0];
                        if(data.action_id) {
                            self.action_manager.doAction(data.action_id[0]);
                            self.menu.open_action(data.action_id[0]);
                        } else {
                            var first_menu_id = self.menu.$el.find("a:first").data("menu");
                            if(first_menu_id) {
                                self.menu.menu_click(first_menu_id);
                            }
                        }
                    });
            });
        } else {
            $(window).trigger('hashchange');
        }
    },
    on_hashchange: function(event) {
        if (this._ignore_hashchange) {
            this._ignore_hashchange = false;
            return;
        }

        var self = this;
        this.clear_uncommitted_changes().then(function () {
            var stringstate = event.getState(false);
            if (!_.isEqual(self._current_state, stringstate)) {
                var state = event.getState(true);
                if(!state.action && state.menu_id) {
                    self.menu.is_bound.done(function() {
                        self.menu.menu_click(state.menu_id);
                    });
                } else {
                    self.action_manager.loadState(state, !!self._current_state).then(function () {
                        var action = self.action_manager.getCurrentAction();
                        if (action) {
                            self.menu.open_action(action.id, state.menu_id);
                        }
                    });
                }
            }
            self._current_state = stringstate;
        }, function () {
            if (event) {
                self._ignore_hashchange = true;
                window.location = event.originalEvent.oldURL;
            }
        });
    },
    on_menu_action: function(options) {
        var self = this;
        return this.menu_dm.add(data_manager.load_action(options.action_id))
            .then(function (result) {
                return self.action_mutex.exec(function() {
                    var completed = $.Deferred();
                    self.action_manager.doAction(result, {
                        clear_breadcrumbs: true,
                        action_menu_id: options.id,
                    }).fail(function() {
                        self.menu.open_menu(options.previous_menu_id);
                    }).always(function() {
                        completed.resolve();
                    });
                    setTimeout(function() {
                        completed.resolve();
                    }, 2000);
                    // We block the menu when clicking on an element until the action has correctly finished
                    // loading. If something crash, there is a 2 seconds timeout before it's unblocked.
                    return completed;
                });
            });
    },
    toggle_fullscreen: function(fullscreen) {
        this._super(fullscreen);
        if (!fullscreen) {
            this.menu.reflow();
        }
    },

    //--------------------------------------------------------------------------
    // Handlers
    //--------------------------------------------------------------------------

    /**
     * @override
     */
    _onGetScrollPosition: function (ev) {
        ev.data.callback({
            left: this.action_manager.el.scrollLeft,
            top: this.action_manager.el.scrollTop,
        });
    },
    /**
     * @override
     */
    _onScrollTo: function (ev) {
        var offset;
        if (ev.data.selector) {
            offset = dom.getPosition(document.querySelector(ev.data.selector));
            // substract the position of the ActionManager as it is the
            // scrolling element
            var actionManagerOffset = dom.getPosition(this.action_manager.el);
            offset.left -= actionManagerOffset.left;
            offset.top -= actionManagerOffset.top;
        } else {
            offset = {top: ev.data.top || 0, left: ev.data.left || 0};
        }

        this.action_manager.el.scrollTop = offset.top;
        this.action_manager.el.scrollLeft = offset.left;
    },
});

});
<commit_msg>[FIX] web: fix a race condition in web client<commit_after>odoo.define('web.WebClient', function (require) {
"use strict";

var AbstractWebClient = require('web.AbstractWebClient');
var config = require('web.config');
var data_manager = require('web.data_manager');
var dom = require('web.dom');
var framework = require('web.framework');
var Menu = require('web.Menu');
var session = require('web.session');
var SystrayMenu = require('web.SystrayMenu');
var UserMenu = require('web.UserMenu');

return AbstractWebClient.extend({
    events: _.extend({}, AbstractWebClient.prototype.events, {
        'click .oe_logo_edit_admin': 'logo_edit',
        'click .oe_logo img': function(ev) {
            ev.preventDefault();
            return this.clear_uncommitted_changes().then(function() {
                framework.redirect("/web" + (config.debug ? "?debug" : ""));
            });
        },
    }),
    show_application: function() {
        var self = this;

        this.toggle_bars(true);
        this.set_title();
        this.update_logo();

        // Menu is rendered server-side thus we don't want the widget to create any dom
        this.menu = new Menu(this);
        this.menu.setElement(this.$el.parents().find('.oe_application_menu_placeholder'));
        this.menu.on('menu_click', this, this.on_menu_action);

        // Create the user menu (rendered client-side)
        this.user_menu = new UserMenu(this);
        var $user_menu_placeholder = $('body').find('.oe_user_menu_placeholder').show();
        var user_menu_loaded = this.user_menu.appendTo($user_menu_placeholder);

        // Create the systray menu (rendered server-side)
        this.systray_menu = new SystrayMenu(this);
        this.systray_menu.setElement(this.$el.parents().find('.oe_systray'));
        var systray_menu_loaded = this.systray_menu.start();

        // Start the menu once both systray and user menus are rendered
        // to prevent overflows while loading
        return $.when(systray_menu_loaded, user_menu_loaded).then(function() {
            self.menu.start();
            self.bind_hashchange();
        });

    },
    toggle_bars: function(value) {
        this.$('tr:has(td.navbar),.oe_leftbar').toggle(value);
    },
    update_logo: function(reload) {
        var company = session.company_id;
        var img = session.url('/web/binary/company_logo' + '?db=' + session.db + (company ? '&company=' + company : ''));
        this.$('.o_sub_menu_logo img').attr('src', '').attr('src', img + (reload ? "&t=" + Date.now() : ''));
        this.$('.oe_logo_edit').toggleClass('oe_logo_edit_admin', session.is_superuser);
    },
    logo_edit: function(ev) {
        var self = this;
        ev.preventDefault();
        this._rpc({
                model: 'res.users',
                method: 'read',
                args: [[session.uid], ['company_id']],
            })
            .then(function(data) {
                self._rpc({
                        route: '/web/action/load',
                        params: { action_id: 'base.action_res_company_form' },
                    })
                    .done(function(result) {
                        result.res_id = data[0].company_id[0];
                        result.target = "new";
                        result.views = [[false, 'form']];
                        result.flags = {
                            action_buttons: true,
                            headless: true,
                        };
                        self.action_manager.doAction(result, {
                            on_close: self.update_logo.bind(self, true),
                        });
                    });
            });
        return false;
    },
    bind_hashchange: function() {
        var self = this;
        $(window).bind('hashchange', this.on_hashchange);
        var didHashChanged = false;
        $(window).one('hashchange', function () {
            didHashChanged = true;
        });

        var state = $.bbq.getState(true);
        if (_.isEmpty(state) || state.action === "login") {
            self.menu.is_bound.done(function() {
                self._rpc({
                        model: 'res.users',
                        method: 'read',
                        args: [[session.uid], ['action_id']],
                    })
                    .done(function(result) {
                        if (didHashChanged) {
                            return;
                        }
                        var data = result[0];
                        if(data.action_id) {
                            self.action_manager.doAction(data.action_id[0]);
                            self.menu.open_action(data.action_id[0]);
                        } else {
                            var first_menu_id = self.menu.$el.find("a:first").data("menu");
                            if(first_menu_id) {
                                self.menu.menu_click(first_menu_id);
                            }
                        }
                    });
            });
        } else {
            $(window).trigger('hashchange');
        }
    },
    on_hashchange: function(event) {
        if (this._ignore_hashchange) {
            this._ignore_hashchange = false;
            return;
        }

        var self = this;
        this.clear_uncommitted_changes().then(function () {
            var stringstate = event.getState(false);
            if (!_.isEqual(self._current_state, stringstate)) {
                var state = event.getState(true);
                if(!state.action && state.menu_id) {
                    self.menu.is_bound.done(function() {
                        self.menu.menu_click(state.menu_id);
                    });
                } else {
                    self.action_manager.loadState(state, !!self._current_state).then(function () {
                        var action = self.action_manager.getCurrentAction();
                        if (action) {
                            self.menu.open_action(action.id, state.menu_id);
                        }
                    });
                }
            }
            self._current_state = stringstate;
        }, function () {
            if (event) {
                self._ignore_hashchange = true;
                window.location = event.originalEvent.oldURL;
            }
        });
    },
    on_menu_action: function(options) {
        this.action_manager.doAction(options.action_id, {
            clear_breadcrumbs: true,
            action_menu_id: options.id,
        });
    },
    toggle_fullscreen: function(fullscreen) {
        this._super(fullscreen);
        if (!fullscreen) {
            this.menu.reflow();
        }
    },

    //--------------------------------------------------------------------------
    // Handlers
    //--------------------------------------------------------------------------

    /**
     * @override
     */
    _onGetScrollPosition: function (ev) {
        ev.data.callback({
            left: this.action_manager.el.scrollLeft,
            top: this.action_manager.el.scrollTop,
        });
    },
    /**
     * @override
     */
    _onScrollTo: function (ev) {
        var offset;
        if (ev.data.selector) {
            offset = dom.getPosition(document.querySelector(ev.data.selector));
            // substract the position of the ActionManager as it is the
            // scrolling element
            var actionManagerOffset = dom.getPosition(this.action_manager.el);
            offset.left -= actionManagerOffset.left;
            offset.top -= actionManagerOffset.top;
        } else {
            offset = {top: ev.data.top || 0, left: ev.data.left || 0};
        }

        this.action_manager.el.scrollTop = offset.top;
        this.action_manager.el.scrollLeft = offset.left;
    },
});

});
<|endoftext|>


<commit_before>/*
 * viperfish
 * https://github.com/mattstyles/viperfish
 *
 * Copyright (c) 2013 Matt Styles
 * Licensed under the MIT license.
 */

// Requires
var vpf         = module.exports = require('./vpf-core'),
    express     = require('express'),
    _           = require('underscore');

/**
 * Init
 * --
 * Object that initialises and creates viperfish
 * @type {Object}
 */
vpf.init = {

    /* -----------------------------------------------------------------------
     *
     *     Set up and create the viperfish object
     *
     * ----------------------------------------------------------------------- */
    /**
     * init
     * adds the various parts of viperfish so it's ready to be properly created
     */
    init: function() {
        // Configure express server - more config happens in init()
        vpf.configure(function(){
            vpf.use(express.logger('dev'));
            vpf.use(express.bodyParser());
            vpf.use(express.methodOverride());
        });

        // Configure for development
        vpf.configure('development', function(){
            vpf.use(express.errorHandler());
        });

        // Add the config to viperfish
        require( './vpf-config' );

        // Add utils to viperfish
        require( './vpf-utils' );

        // Add core functionality to viperfish
        require( './vpf-core' );

        // @todo add the required modules here - modules work by over-riding the functions declared with the require
        // @todo calls performed above
        _.extend( vpf.core, require( 'octofish' ) );

        // Call the create function to create viperfish now that all the functions have been added to vpf
        vpf.init.create();
    },

    /**
     * create
     * once the functions are defined by init() this function calls them all to create viperfish
     */
    create: function() {
        // Authorise with github
        //vpf.core.authGithub();
        vpf.core.authorise();

        // Set the config first
        vpf.core.setConfig( 'config.js' );

        // Get nav data and store it in the config
        vpf.core.getNav();

        // Set the templates - custom if they exist or default
        vpf.core.setTemplates();

        // Set up the port number - only used if not specified by process
        vpf.core.setPort( vpf.config.moduleOpts.port );

        // Set up static serving
        vpf.core.setStatic( vpf.config.moduleOpts.customPath );

        // Set up favicon
        vpf.core.setFavicon( vpf.config.moduleOpts.favicon );

        // Configure routes for viperfish after configuring static serving
        require( './vpf-router' );
    }
};<commit_msg>Add the actual code in init that injects the modules into the core<commit_after>/*
 * viperfish
 * https://github.com/mattstyles/viperfish
 *
 * Copyright (c) 2013 Matt Styles
 * Licensed under the MIT license.
 */

// Requires
var vpf         = module.exports = require('./vpf-core'),
    express     = require('express'),
    _           = require('underscore');

/**
 * Init
 * --
 * Object that initialises and creates viperfish
 * @type {Object}
 */
vpf.init = {

    /* -----------------------------------------------------------------------
     *
     *     Set up and create the viperfish object
     *
     * ----------------------------------------------------------------------- */
    /**
     * init
     * adds the various parts of viperfish so it's ready to be properly created
     */
    init: function() {
        // Configure express server - more config happens in init()
        vpf.configure(function(){
            vpf.use(express.logger('dev'));
            vpf.use(express.bodyParser());
            vpf.use(express.methodOverride());
        });

        // Configure for development
        vpf.configure('development', function(){
            vpf.use(express.errorHandler());
        });

        // Add the config to viperfish
        require( './vpf-config' );

        // Add utils to viperfish
        require( './vpf-utils' );

        // Add core functionality to viperfish
        require( './vpf-core' );

        // Add modules that were required before starting viperfish
        // For each module add it to the core - this makes the order in which they are required important
        _.each( vpf.modules, function( module ) {
            _.extend( vpf.core, require( module ) );
        });

        // Call the create function to create viperfish now that all the functions have been added to vpf
        vpf.init.create();
    },

    /**
     * create
     * once the functions are defined by init() this function calls them all to create viperfish
     */
    create: function() {
        // Authorise with github
        //vpf.core.authGithub();
        vpf.core.authorise();

        // Set the config first
        vpf.core.setConfig( 'config.js' );

        // Get nav data and store it in the config
        vpf.core.getNav();

        // Set the templates - custom if they exist or default
        vpf.core.setTemplates();

        // Set up the port number - only used if not specified by process
        vpf.core.setPort( vpf.config.moduleOpts.port );

        // Set up static serving
        vpf.core.setStatic( vpf.config.moduleOpts.customPath );

        // Set up favicon
        vpf.core.setFavicon( vpf.config.moduleOpts.favicon );

        // Configure routes for viperfish after configuring static serving
        require( './vpf-router' );
    }
};<|endoftext|>


<commit_before>(function () {
    'use strict';

    angular
        .module('dpShared')
        .factory('authenticator', authenticatorFactory);

    authenticatorFactory.$inject = [
        '$interval',
        'sharedConfig',
        'user',
        '$window',
        '$location',
        'storage',
        'uriStripper',
        'httpStatus',
        'stateTokenGenerator',
        'queryStringParser',
        'Raven'
    ];

    // eslint-disable-next-line max-params
    function authenticatorFactory (
        $interval,
        sharedConfig,
        user,
        $window,
        $location,
        storage,
        uriStripper,
        httpStatus,
        stateTokenGenerator,
        queryStringParser,
        Raven
    ) {
        // A map of the error keys, that the OAuth2 authorization service can
        // return, to a full description
        const ERROR_MESSAGES = {
            invalid_request: 'The request is missing a required parameter, includes an invalid parameter value, ' +
                'includes a parameter more than once, or is otherwise malformed.',
            unauthorized_client: 'The client is not authorized to request an access token using this method.',
            access_denied: 'The resource owner or authorization server denied the request.',
            unsupported_response_type: 'The authorization server does not support obtaining an access token using ' +
                'this method.',
            invalid_scope: 'The requested scope is invalid, unknown, or malformed.',
            server_error: 'The authorization server encountered an unexpected condition that prevented it from ' +
                'fulfilling the request.',
            temporarily_unavailable: 'The authorization server is currently unable to handle the request due to a ' +
                'temporary overloading or maintenance of the server.'
        };

        // The parameters the OAuth2 authorization service will return on
        // success
        const AUTH_PARAMS = ['access_token', 'token_type', 'expires_in', 'state'];

        // The URI we need to redirect to for communication with the OAuth2
        // authorization service
        const AUTH_PATH = 'oauth2/';
        const LOGIN_PATH = 'authorize?idp_id=datapunt&response_type=token&client_id=citydata-data.amsterdam.nl';

        // The keys of values we need to store in the session storage
        //
        // Query string of the state at the moment we redirect to the OAuth2
        // authorization service, and need to get back to afterwards
        const CALLBACK_PARAMS = 'callbackParams';
        // The OAuth2 state(token) (OAuth terminology, has nothing to do with
        // our app state), which is a random string
        const STATE_TOKEN = 'stateToken';
        // The access token returned by the OAuth2 authorization service
        // containing user scopes and name
        const ACCESS_TOKEN = 'accessToken';

        let initialized = false;

        return {
            initialize,
            login,
            logout
        };

        function initialize () {
            if (!initialized) {
                initialized = true;
                restoreAccessToken(); // Restore acces token from session storage
                catchError(); // Catch any error from the OAuth2 authorization service
                handleCallback(); // Handle a callback from the OAuth2 authorization service
            }
        }

        /**
         * Redirects to the OAuth2 authorization service.
         */
        function login () {
            // Get the URI the OAuth2 authorization service needs to use as
            // callback
            const callback = $location.absUrl().replace(/#.*$/, ''); // Remove all parameters
            const stateToken = $window.encodeURIComponent(stateTokenGenerator()); // Get a random string to prevent CSRF

            if (!stateToken) {
                // crypto library is not available on the current browser
                httpStatus.registerError(httpStatus.LOGIN_ERROR);
                return;
            }

            savePath(); // Save current path in session
            saveStateToken(stateToken); // Save the state token in session

            $window.location.href =
                sharedConfig.API_ROOT + AUTH_PATH + LOGIN_PATH +
                `&state=${stateToken}&redirect_uri=${encodeURIComponent(callback)}`;
        }

        /**
         * Removes the access token from the user and the session storage.
         */
        function logout () {
            user.clearToken();
            removeAccessToken();
        }

        /**
         * Do the given params form a callback from the OAuth2 authorization
         * service?
         *
         * @param {Object.<string, string>} params The parameters returned.
         * @return boolean True when all `AUTH_PARAMS` are available and the
         * `state` param equals to the value set in the session storage,
         * otherwise false.
         */
        function isCallback (params) {
            if (!params) {
                return false;
            }

            // The state param must be exactly the same as the state token we
            // have saved in the session (to prevent CSRF)
            const stateTokenValid = params.state && params.state === getStateToken();

            // It is a callback when all authorization parameters are defined
            // in the params the fastest check is not to check if all
            // parameters are defined but to check that no undefined parameter
            // can be found
            const paramsValid = !AUTH_PARAMS.some((param) => {
                return angular.isUndefined(params[param]);
            });

            if (paramsValid && !stateTokenValid) {
                // This is a callback, but the state token does not equal the
                // one we have saved; report to Sentry
                Raven.captureMessage(new Error(`Authenticator encountered an invalid state token (${params.state})`));
            }

            return Boolean(stateTokenValid && paramsValid);
        }

        /**
         * Gets the access token in case we have a valid callback. Uses the
         * authorization parameters from the access token.
         *
         * @returns boolean True when this is a callback, otherwise false.
         */
        function handleCallback () {
            const params = queryStringParser($location.url());
            if (isCallback(params)) {
                useAccessToken(params.access_token);
                return true;
            }
            return false;
        }

        /**
         * Restores the access token from session storage when available.
         */
        function restoreAccessToken () {
            const accessToken = getAccessToken();
            if (accessToken) {
                user.setAccessToken(accessToken);
            }
        }

        /**
         * Finishes the callback from the OAuth2 authorization service.
         */
        function useAccessToken (token) {
            user.setAccessToken(token);
            saveAccessToken(token);
            removeStateToken(); // Remove state token from session
            const pathParams = storage.session.getItem(CALLBACK_PARAMS);
            if (pathParams) {
                restorePath(pathParams); // Restore path from session
            }
        }

        /**
         * Handles errors in case they were returned by the OAuth2
         * authorization service.
         */
        function catchError () {
            const params = queryStringParser($window.location.search);
            if (params && params.error) {
                handleError(params.error, params.error_description);
            }
        }

        /**
         * Finishes an error from the OAuth2 authorization service.
         *
         * @param code {string} Error code as returned from the service.
         * @param description {string} Error description as returned from the
         * service.
         */
        function handleError (code, description) {
            Raven.captureMessage(new Error(
                `Authorization service responded with error ${code} [${description}] (${ERROR_MESSAGES[code]})`));
            removeStateToken(); // Remove state token from session
            restorePath(storage.session.getItem(CALLBACK_PARAMS)); // Restore path from session
            removeErrorParamsFromPath();
        }

        /**
         * Saves the current search parameters (our state in the URL) to the
         * session storage.
         */
        function savePath () {
            const params = $location.search();
            if (params.dte) {
                // $location.search may return old parameters even though URL does not list them.
                // This mean the dte parameter may incorrectly contain the domain:
                // e.g.: https://data.amsterdam.nl/foo/bar instead of foo/bar.
                // This step ensures no domain and protocol are stored, so the correct path is restored after login.
                // Seems like an Angular bug when not using HTML5 $location provider. Consider removing when/if
                // HTML5 location mode is activated. https://github.com/angular/angular.js/issues/1521
                params.dte = uriStripper.stripDomain(params.dte);
            }
            storage.session.setItem(CALLBACK_PARAMS, angular.toJson(params)); // encode params
        }

        /**
         * Restores saved search parameters from the session storage (by
         * `savePath`) to the URL.
         *
         * @param {string} paramString The parameter string as saved in the
         * session storage.
         */
        function restorePath (paramString) {
            storage.session.removeItem(CALLBACK_PARAMS);
            const params = paramString ? angular.fromJson(paramString) : {};

            $location.replace(); // overwrite the existing location (prevent back button to re-login)
            $location.url(''); // remove the parameters from the authorization service
            $location.search(params);
        }

        /**
         * Removes parameters from the URL, as set by an error callback from
         * the OAuth2 authorization service, to clean up the URL.
         */
        function removeErrorParamsFromPath () {
            $interval(
                () => $window.location.href = $window.location.protocol + '//' +
                    $window.location.host + $window.location.pathname +
                    '#' + $location.url(),
                0, 1);
        }

        function saveStateToken (stateToken) {
            storage.session.setItem(STATE_TOKEN, stateToken);
        }

        function getStateToken () {
            return storage.session.getItem(STATE_TOKEN);
        }

        function removeStateToken () {
            storage.session.removeItem(STATE_TOKEN);
        }

        function saveAccessToken (accessToken) {
            storage.session.setItem(ACCESS_TOKEN, accessToken);
        }

        function getAccessToken () {
            return storage.session.getItem(ACCESS_TOKEN);
        }

        function removeAccessToken (accessToken) {
            storage.session.removeItem(ACCESS_TOKEN);
        }
    }
})();
<commit_msg>Updated client id<commit_after>(function () {
    'use strict';

    angular
        .module('dpShared')
        .factory('authenticator', authenticatorFactory);

    authenticatorFactory.$inject = [
        '$interval',
        'sharedConfig',
        'user',
        '$window',
        '$location',
        'storage',
        'uriStripper',
        'httpStatus',
        'stateTokenGenerator',
        'queryStringParser',
        'Raven'
    ];

    // eslint-disable-next-line max-params
    function authenticatorFactory (
        $interval,
        sharedConfig,
        user,
        $window,
        $location,
        storage,
        uriStripper,
        httpStatus,
        stateTokenGenerator,
        queryStringParser,
        Raven
    ) {
        // A map of the error keys, that the OAuth2 authorization service can
        // return, to a full description
        const ERROR_MESSAGES = {
            invalid_request: 'The request is missing a required parameter, includes an invalid parameter value, ' +
                'includes a parameter more than once, or is otherwise malformed.',
            unauthorized_client: 'The client is not authorized to request an access token using this method.',
            access_denied: 'The resource owner or authorization server denied the request.',
            unsupported_response_type: 'The authorization server does not support obtaining an access token using ' +
                'this method.',
            invalid_scope: 'The requested scope is invalid, unknown, or malformed.',
            server_error: 'The authorization server encountered an unexpected condition that prevented it from ' +
                'fulfilling the request.',
            temporarily_unavailable: 'The authorization server is currently unable to handle the request due to a ' +
                'temporary overloading or maintenance of the server.'
        };

        // The parameters the OAuth2 authorization service will return on
        // success
        const AUTH_PARAMS = ['access_token', 'token_type', 'expires_in', 'state'];

        // The URI we need to redirect to for communication with the OAuth2
        // authorization service
        const AUTH_PATH = 'oauth2/';
        const LOGIN_PATH = 'authorize?idp_id=datapunt&response_type=token&client_id=citydata';

        // The keys of values we need to store in the session storage
        //
        // Query string of the state at the moment we redirect to the OAuth2
        // authorization service, and need to get back to afterwards
        const CALLBACK_PARAMS = 'callbackParams';
        // The OAuth2 state(token) (OAuth terminology, has nothing to do with
        // our app state), which is a random string
        const STATE_TOKEN = 'stateToken';
        // The access token returned by the OAuth2 authorization service
        // containing user scopes and name
        const ACCESS_TOKEN = 'accessToken';

        let initialized = false;

        return {
            initialize,
            login,
            logout
        };

        function initialize () {
            if (!initialized) {
                initialized = true;
                restoreAccessToken(); // Restore acces token from session storage
                catchError(); // Catch any error from the OAuth2 authorization service
                handleCallback(); // Handle a callback from the OAuth2 authorization service
            }
        }

        /**
         * Redirects to the OAuth2 authorization service.
         */
        function login () {
            // Get the URI the OAuth2 authorization service needs to use as
            // callback
            const callback = $location.absUrl().replace(/#.*$/, ''); // Remove all parameters
            const stateToken = $window.encodeURIComponent(stateTokenGenerator()); // Get a random string to prevent CSRF

            if (!stateToken) {
                // crypto library is not available on the current browser
                httpStatus.registerError(httpStatus.LOGIN_ERROR);
                return;
            }

            savePath(); // Save current path in session
            saveStateToken(stateToken); // Save the state token in session

            $window.location.href =
                sharedConfig.API_ROOT + AUTH_PATH + LOGIN_PATH +
                `&state=${stateToken}&redirect_uri=${encodeURIComponent(callback)}`;
        }

        /**
         * Removes the access token from the user and the session storage.
         */
        function logout () {
            user.clearToken();
            removeAccessToken();
        }

        /**
         * Do the given params form a callback from the OAuth2 authorization
         * service?
         *
         * @param {Object.<string, string>} params The parameters returned.
         * @return boolean True when all `AUTH_PARAMS` are available and the
         * `state` param equals to the value set in the session storage,
         * otherwise false.
         */
        function isCallback (params) {
            if (!params) {
                return false;
            }

            // The state param must be exactly the same as the state token we
            // have saved in the session (to prevent CSRF)
            const stateTokenValid = params.state && params.state === getStateToken();

            // It is a callback when all authorization parameters are defined
            // in the params the fastest check is not to check if all
            // parameters are defined but to check that no undefined parameter
            // can be found
            const paramsValid = !AUTH_PARAMS.some((param) => {
                return angular.isUndefined(params[param]);
            });

            if (paramsValid && !stateTokenValid) {
                // This is a callback, but the state token does not equal the
                // one we have saved; report to Sentry
                Raven.captureMessage(new Error(`Authenticator encountered an invalid state token (${params.state})`));
            }

            return Boolean(stateTokenValid && paramsValid);
        }

        /**
         * Gets the access token in case we have a valid callback. Uses the
         * authorization parameters from the access token.
         *
         * @returns boolean True when this is a callback, otherwise false.
         */
        function handleCallback () {
            const params = queryStringParser($location.url());
            if (isCallback(params)) {
                useAccessToken(params.access_token);
                return true;
            }
            return false;
        }

        /**
         * Restores the access token from session storage when available.
         */
        function restoreAccessToken () {
            const accessToken = getAccessToken();
            if (accessToken) {
                user.setAccessToken(accessToken);
            }
        }

        /**
         * Finishes the callback from the OAuth2 authorization service.
         */
        function useAccessToken (token) {
            user.setAccessToken(token);
            saveAccessToken(token);
            removeStateToken(); // Remove state token from session
            const pathParams = storage.session.getItem(CALLBACK_PARAMS);
            if (pathParams) {
                restorePath(pathParams); // Restore path from session
            }
        }

        /**
         * Handles errors in case they were returned by the OAuth2
         * authorization service.
         */
        function catchError () {
            const params = queryStringParser($window.location.search);
            if (params && params.error) {
                handleError(params.error, params.error_description);
            }
        }

        /**
         * Finishes an error from the OAuth2 authorization service.
         *
         * @param code {string} Error code as returned from the service.
         * @param description {string} Error description as returned from the
         * service.
         */
        function handleError (code, description) {
            Raven.captureMessage(new Error(
                `Authorization service responded with error ${code} [${description}] (${ERROR_MESSAGES[code]})`));
            removeStateToken(); // Remove state token from session
            restorePath(storage.session.getItem(CALLBACK_PARAMS)); // Restore path from session
            removeErrorParamsFromPath();
        }

        /**
         * Saves the current search parameters (our state in the URL) to the
         * session storage.
         */
        function savePath () {
            const params = $location.search();
            if (params.dte) {
                // $location.search may return old parameters even though URL does not list them.
                // This mean the dte parameter may incorrectly contain the domain:
                // e.g.: https://data.amsterdam.nl/foo/bar instead of foo/bar.
                // This step ensures no domain and protocol are stored, so the correct path is restored after login.
                // Seems like an Angular bug when not using HTML5 $location provider. Consider removing when/if
                // HTML5 location mode is activated. https://github.com/angular/angular.js/issues/1521
                params.dte = uriStripper.stripDomain(params.dte);
            }
            storage.session.setItem(CALLBACK_PARAMS, angular.toJson(params)); // encode params
        }

        /**
         * Restores saved search parameters from the session storage (by
         * `savePath`) to the URL.
         *
         * @param {string} paramString The parameter string as saved in the
         * session storage.
         */
        function restorePath (paramString) {
            storage.session.removeItem(CALLBACK_PARAMS);
            const params = paramString ? angular.fromJson(paramString) : {};

            $location.replace(); // overwrite the existing location (prevent back button to re-login)
            $location.url(''); // remove the parameters from the authorization service
            $location.search(params);
        }

        /**
         * Removes parameters from the URL, as set by an error callback from
         * the OAuth2 authorization service, to clean up the URL.
         */
        function removeErrorParamsFromPath () {
            $interval(
                () => $window.location.href = $window.location.protocol + '//' +
                    $window.location.host + $window.location.pathname +
                    '#' + $location.url(),
                0, 1);
        }

        function saveStateToken (stateToken) {
            storage.session.setItem(STATE_TOKEN, stateToken);
        }

        function getStateToken () {
            return storage.session.getItem(STATE_TOKEN);
        }

        function removeStateToken () {
            storage.session.removeItem(STATE_TOKEN);
        }

        function saveAccessToken (accessToken) {
            storage.session.setItem(ACCESS_TOKEN, accessToken);
        }

        function getAccessToken () {
            return storage.session.getItem(ACCESS_TOKEN);
        }

        function removeAccessToken (accessToken) {
            storage.session.removeItem(ACCESS_TOKEN);
        }
    }
})();
<|endoftext|>


<commit_before>// whiskers.js templating library

// the encapsulating function
(function(whiskers) {
  // for compiled templates
  whiskers.cache = {};

  // main function
  whiskers.render = function(template, context, partials) {
    template = whiskers.expand(template, partials);
    context = context || {};
    // use cached if it exists
    if (whiskers.cache[template]) return whiskers.cache[template](context);
    // compile and cache
    whiskers.cache[template] = whiskers.compile(template);
    return whiskers.cache[template](context);
  };

  // constant to guard against infinite recursion
  whiskers.MAX_TEMPLATE_DEPTH = 20;

  // expand all partials
  whiskers.expand = function(template, partials) {
    // helper function for getting partials with dot notation
    var getPartial = function(partials, key) {
      var accessors = key.split('.');
      for (var i=0, len=accessors.length; i<len; i++) {
        if (!partials) break;
        partials = partials[accessors[i]];
      }
      return partials || '';
    };
    // recursive function
    var expandr = function(template, partials, indent, depth) {
      // convert to string, empty if false
      template = (template || '') + '';
      // drop trailing newlines and prepend indent to each line
      indent = indent || '';
      template = indent + template.replace(/\n+$/, '').replace(/\n/g, indent || '\n');
      // handle template depth
      depth = depth || 0;
      if (depth > whiskers.MAX_TEMPLATE_DEPTH) {
        console.warn('maximum template depth reached');
        return template;
      }
      depth++;
      // replace partial tags
      return template.replace(/(\n[ \t]+|)(\\*){>([\w.]+)}/g, function(str, indent, escapeChar, key) {
        // if tag is escaped return it untouched with first backslash removed
        if (escapeChar) return str.replace('\\', '');
        template = getPartial(partials, key);
        // recurse to expand any partials in this partial
        return expandr(template, partials, indent, depth);
      });
    };
    return expandr(template, partials);
  };

  // compile template to function
  whiskers.compile = function(template) {
    // convert to string, empty if false
    template = (template || '') + '';

    // escape backslashes, single quotes, and newlines
    template = template.replace(/\\/g, '\\\\').replace(/\'/g, '\\\'').replace(/\n/g, '\\n');

    // replace comment tags (like {!foo!})
    template = template.replace(/(\\n[ \t]*|)(\\*){![\s\S]*?!}(?:([ \t]*(?=\\n))|)/, function(str, newlineBefore, escapeChar, newlineAfter) {
      if (escapeChar) return str.replace('\\\\', '');
      // remove preceding newline and whitespace if newline also after
      if (newlineBefore && newlineAfter !== undefined) {
        newlineBefore = '';
      }
      return newlineBefore+'';
    });

    // replace simple variable tags (like {bar})
    template = template.replace(/(\\*){([\w.]+)}/g, function(str, escapeChar, key) {
      if (escapeChar) return str.replace('\\\\', '');
      return '\'+g(c,\''+key+'\')+\'';
    });

    // to keep track of the logic
    var stack = [], block;

    // replace logic tags (like {for foo in bar} and {if foo})
    template = template.replace(/(\\n[ \t]*|)(\\*){(\/?)(for|if|)( +|)(not +|)((\w+) +in +|)([\w.]+|)}(?:([ \t]*(?=\\n))|)/g, function(str, newlineBefore, escapeChar, close, statement, statementSpace, ifNot, forIn, iterVar, key, newlineAfter, offset, s) {
      if (escapeChar) return str.replace('\\\\', '');
      // remove preceding newline and whitespace for standalone logic tag
      if (newlineBefore && newlineAfter !== undefined) {
        newlineBefore = '';
      }
      // opening tags
      if (statement && statementSpace && key) {
        stack.push({statement:statement});
        // {for foo in bar}
        if (statement == 'for' && iterVar) {
          return newlineBefore+'\';var '+iterVar+'A=g(c,\''+key+'\');for(var '+iterVar+'I=0;'+iterVar+'I<'+iterVar+'A.length;'+iterVar+'I++){c[\''+iterVar+'\']='+iterVar+'A['+iterVar+'I];b+=\'';
        }
        // {if foo} or {if not foo}
        if (statement == 'if') {
          return newlineBefore+'\';if('+(ifNot?'!':'')+'g(c,\''+key+'\')){b+=\'';
        }
      }
      // closing tags ({/for} or {/if})
      if (close && statement) {
        block = stack[stack.length-1];
        if (block && block.statement == statement) {
          stack.pop();
          return newlineBefore+'\';}b+=\'';
        }
        console.warn('extra {/'+statement+'} ignored');
        return '';
      }
      // not a tag, don't replace
      return str;
    });

    // close extra fors and ifs
    for (var i=stack.length-1; i>-1; i--) {
      block = stack[i];
      console.warn('extra "'+block.statement+'" closed at end of template');
      template = template+'\';}b+=\'';
    }

    // c is context, g is get, o is object, k is key, a is accessors, b is buffer
    return new Function('c', 'var g=function(o,k){var a=k.split(\'.\');for(var i=0,l=a.length;i<l;i++){o=o[a[i]];if(!o)break}if(typeof o==\'function\')o=o();if(o===undefined)o=\'\';return o};var b=\''+template+'\';return b');
  };
})(typeof module == 'object' ? module.exports : this.whiskers = {});
<commit_msg>Better logic regex -- no false positives<commit_after>// whiskers.js templating library

// the encapsulating function
(function(whiskers) {
  // for compiled templates
  whiskers.cache = {};

  // main function
  whiskers.render = function(template, context, partials) {
    template = whiskers.expand(template, partials);
    context = context || {};
    // use cached if it exists
    if (whiskers.cache[template]) return whiskers.cache[template](context);
    // compile and cache
    whiskers.cache[template] = whiskers.compile(template);
    return whiskers.cache[template](context);
  };

  // constant to guard against infinite recursion
  whiskers.MAX_TEMPLATE_DEPTH = 20;

  // expand all partials
  whiskers.expand = function(template, partials) {
    // helper function for getting partials with dot notation
    var getPartial = function(partials, key) {
      var accessors = key.split('.');
      for (var i=0, len=accessors.length; i<len; i++) {
        if (!partials) break;
        partials = partials[accessors[i]];
      }
      return partials || '';
    };
    // recursive function
    var expandr = function(template, partials, indent, depth) {
      // convert to string, empty if false
      template = (template || '') + '';
      // drop trailing newlines and prepend indent to each line
      indent = indent || '';
      template = indent + template.replace(/\n+$/, '').replace(/\n/g, indent || '\n');
      // handle template depth
      depth = depth || 0;
      if (depth > whiskers.MAX_TEMPLATE_DEPTH) {
        console.warn('maximum template depth reached');
        return template;
      }
      depth++;
      // replace partial tags
      return template.replace(/(\n[ \t]+|)(\\*){>([\w.]+)}/g, function(str, indent, escapeChar, key) {
        // if tag is escaped return it untouched with first backslash removed
        if (escapeChar) return str.replace('\\', '');
        template = getPartial(partials, key);
        // recurse to expand any partials in this partial
        return expandr(template, partials, indent, depth);
      });
    };
    return expandr(template, partials);
  };

  // compile template to function
  whiskers.compile = function(template) {
    // convert to string, empty if false
    template = (template || '') + '';

    // escape backslashes, single quotes, and newlines
    template = template.replace(/\\/g, '\\\\').replace(/\'/g, '\\\'').replace(/\n/g, '\\n');

    // replace comment tags (like {!foo!})
    template = template.replace(/(\\n[ \t]*|)(\\*){![\s\S]*?!}(?:([ \t]*(?=\\n))|)/, function(str, newlineBefore, escapeChar, newlineAfter) {
      if (escapeChar) return str.replace('\\\\', '');
      // remove preceding newline and whitespace if newline also after
      if (newlineBefore && newlineAfter !== undefined) {
        newlineBefore = '';
      }
      return newlineBefore+'';
    });

    // replace simple variable tags (like {bar})
    template = template.replace(/(\\*){([\w.]+)}/g, function(str, escapeChar, key) {
      if (escapeChar) return str.replace('\\\\', '');
      return '\'+g(c,\''+key+'\')+\'';
    });

    // to keep track of the logic
    var stack = [], block;

    // replace logic tags (like {for foo in bar} and {if foo})
    template = template.replace(/(\\n[ \t]*|)(\\*){(?:for +(\w+) +in +([\w.]+)|if +(not +|)([\w.]+)|\/(for|if))}(?:([ \t]*(?=\\n))|)/g, function(str, newlineBefore, escapeChar, iterVar, forKey, ifNot, ifKey, closeStatement, newlineAfter, offset, s) {
      if (escapeChar) return str.replace('\\\\', '');
      // remove preceding newline and whitespace for standalone logic tag
      if (newlineBefore && newlineAfter !== undefined) {
        newlineBefore = '';
      }
      // {for foo in bar}
      if (forKey) {
        stack.push({statement:'for'});
        return newlineBefore+'\';var '+iterVar+'A=g(c,\''+forKey+'\');for(var '+iterVar+'I=0;'+iterVar+'I<'+iterVar+'A.length;'+iterVar+'I++){c[\''+iterVar+'\']='+iterVar+'A['+iterVar+'I];b+=\'';
      }
      // {if foo} or {if not foo}
      if (ifKey) {
        stack.push({statement:'if'});
        return newlineBefore+'\';if('+(ifNot?'!':'')+'g(c,\''+ifKey+'\')){b+=\'';
      }
      // closing tags ({/for} or {/if})
      if (closeStatement) {
        block = stack[stack.length-1];
        if (block && block.statement == closeStatement) {
          stack.pop();
          return newlineBefore+'\';}b+=\'';
        }
        console.warn('extra {/'+closeStatement+'} ignored');
        return '';
      }
      // not a tag, don't replace
      return str;
    });

    // close extra fors and ifs
    for (var i=stack.length-1; i>-1; i--) {
      block = stack[i];
      console.warn('extra "'+block.statement+'" closed at end of template');
      template = template+'\';}b+=\'';
    }

    // c is context, g is get, o is object, k is key, a is accessors, b is buffer
    return new Function('c', 'var g=function(o,k){var a=k.split(\'.\');for(var i=0,l=a.length;i<l;i++){o=o[a[i]];if(!o)break}if(typeof o==\'function\')o=o();if(o===undefined)o=\'\';return o};var b=\''+template+'\';return b');
  };
})(typeof module == 'object' ? module.exports : this.whiskers = {});
<|endoftext|>


<commit_before>"use strict";

const tcoerce = require("tiny-coerce"),
	lru = require("tiny-lru"),
	http = require("http"),
	parse = require("tiny-parse"),
	mmh3 = require("murmurhash3js").x86.hash32,
	all = "all",
	delimiter = ":",
	methods = http.METHODS,
	regex = {
		braceLeft: /\(/,
		hasGet: /GET/,
		hasParam: /\/:(\w*)/,
		isHead: /^HEAD$/,
		isParam: /^:/,
		isOptions: /^OPTIONS$/,
		slashEnd: /\/$/,
		slashStart: /^\//
	};

function clone (arg) {
	return JSON.parse(JSON.stringify(arg));
}

function each (arr, fn) {
	const nth = arr.length;
	let i = -1;

	while (++i < nth) {
		fn(arr[i], i);
	}

	return arr;
}

class Woodland {
	constructor (defaultHeaders, cacheSize, seed, coerce, http2) {
		this.blacklisted = new Set();
		this.cache = lru(cacheSize);
		this.coerce = coerce;
		this.http2 = http2;
		this.defaultHeaders = Object.keys(defaultHeaders).map(key => [key.toLowerCase(), defaultHeaders[key]]);
		this.permissions = lru(cacheSize);
		this.middleware = new Map();
		this.seed = seed;
	}

	allowed (method, uri, override = false) {
		return this.routes(uri, method, override).middleware.filter(i => this.blacklisted.has(i.hash) === false).length > 0;
	}

	allows (uri, override = false) {
		const key = uri;
		let result = override === false ? this.permissions.get(key) : void 0;

		if (override === true || result === void 0) {
			const list = new Set(methods.filter(i => this.allowed(i, uri, override)));

			if (list.has("GET")) {
				list.add("HEAD");
				list.add("OPTIONS");
			}

			result = Array.from(list).sort().join(", ");
			this.permissions.set(key, result);
		}

		return result;
	}

	blacklist (fn) {
		if (this.blacklisted.has(fn.hash) === false) {
			this.blacklisted.add(fn.hash);
		}

		return this;
	}

	decorate (req, res) {
		const parsed = parse(req, this.coerce),
			remotes = req.headers["x-forwarded-for"] !== void 0 ? req.headers["x-forwarded-for"].split(/\s*,\s*/g) : [req.connection.remoteAddress];

		if (this.http2) {
			res.tempHeaders = {};

			res.setHeader = (key, value) => {
				res.tempHeaders[key] = value;
			};

			res.send = (body, status = 200, headers = null) => {
				if (res.writable) {
					const output = {":status": status};

					each(Object.keys(res.tempHeaders), i => {
						output[i] = res.tempHeaders[i];
					});

					res.tempHeaders = {};

					if (headers !== null) {
						each(Object.keys(headers), i => {
							output[i] = headers[i];
						});
					}

					res.respond(output);
					res.end(body);
				}
			};
		}

		// Express interop
		res.header = res.setHeader;
		res.locals = {};
		req.params = {};

		// Decorating useful hints
		req.body = "";
		req.ip = remotes[remotes.length - 1];
		req.parsed = parsed;
		req.query = parsed.query;
		req.host = req.parsed.hostname;
		req.allow = this.allows(req.parsed.pathname);
		req.cors = req.headers.origin !== void 0 && this.schemeless(req.headers.host) !== this.schemeless(req.headers.origin);

		// CORS handling
		if (req.cors === true) {
			let headers = req.headers["access-control-request-headers"];

			res.header("access-control-allow-origin", req.headers.origin);
			res.header("access-control-allow-credentials", "true");

			if (headers !== void 0) {
				res.header(regex.isOptions.test(req.method) ? "access-control-allow-headers" : "access-control-expose-headers", headers);
			}

			if (req.allow !== "") {
				res.header("access-control-allow-methods", req.allow);
			}
		}

		// Setting headers
		each(this.defaultHeaders, i => res.header(i[0], i[1]));

		if (req.allow !== "") {
			res.header("allow", req.allow);
		}
	}

	hash (arg) {
		return mmh3(arg, this.seed);
	}

	list (method = "get", type = "array") {
		let result;

		if (type === "array") {
			result = Array.from(this.middleware.get(method.toUpperCase()).keys());
		} else if (type === "object") {
			result = {};

			this.middleware.get(method.toUpperCase()).forEach((value, key) => {
				result[key] = value;
			});
		}

		return result;
	}

	normalize (arg) {
		const obj = clone(arg);

		obj.method = obj.headers[":method"];
		obj.connection = {remoteAddress: "0.0.0.0"};
		obj.url = obj.headers[":path"]; //`${obj.headers[":scheme"]}//${obj.headers[":authority"]}${obj.headers[":path"]}`;

		delete obj.headers[":authority"];
		delete obj.headers[":method"];
		delete obj.headers[":path"];
		delete obj.headers[":scheme"];

		return obj;
	}

	onclose () {}

	onconnect () {}

	onerror (req, res, err) {
		if (this.http2 === false) {
			const numeric = isNaN(err.message) === false,
				status = isNaN(res.statusCode) === false && res.statusCode >= 400 ? res.statusCode : numeric ? Number(err.message) : 500,
				output = numeric ? http.STATUS_CODES[status] : err.message;

			res.statusCode = status;
			res.writeHead(status, {"content-type": "text/plain", "content-length": Buffer.byteLength(output)});
			res.end(output);
		} else {
			const numeric = isNaN(err.message) === false,
				status = numeric ? Number(err.message) : 500,
				output = numeric ? http.STATUS_CODES[status] : err.message;

			res.send(output, status, {"content-type": "text/plain"});
		}
	}

	onfinish () {}

	params (...args) {
		if (this.http2 === false) {
			const [req, pos = []] = args,
				uri = req.parsed.path.replace(regex.slashEnd, "").replace(regex.slashEnd, "").split("/");

			each(pos, i => {
				req.params[i[1]] = tcoerce(uri[i[0]]);
			});
		}
	}

	route (...args) {
		const [req, res] = this.http2 === false ? args : [this.normalize({headers: args[1]}), args[0]];

		return new Promise((resolve, reject) => {
			let method = regex.isHead.test(req.method) ? "GET" : req.method,
				mregex = new RegExp(method),
				middleware, result;

			const last = err => {
				if (err === void 0) {
					reject(new Error(regex.hasGet.test(req.allow || "") ? 405 : 404));
				} else if (isNaN(res.statusCode) === false && res.statusCode >= 400) {
					reject(err);
				} else {
					reject(new Error(res.statusCode >= 400 ? res.statusCode : isNaN(err.message) === false ? err.message : http.STATUS_CODES[err.message || err] || 500));
				}
			};

			const next = err => {
				process.nextTick(() => {
					let iter = middleware.next(),
						arity = iter.done === false ? iter.value.length : 0;

					if (iter.done === false) {
						if (err !== void 0) {
							do {
								arity = iter.value.length;
							} while (arity < 4 && (iter = middleware.next()) && iter.done === false);
						}

						if (iter.done === false) {
							if (err !== void 0 && arity === 4) {
								try {
									iter.value(err, req, res, next);
								} catch (e) {
									next(e);
								}
							} else if (err === void 0 && arity <= 3) {
								try {
									iter.value(req, res, next);
								} catch (e) {
									next(e);
								}
							} else {
								next(); // Error handler in the middle
							}
						} else {
							last(err); // Unhandled error
						}
					} else {
						last(err); // Called next() in the last middleware
					}
				});
			};

			res.on("finish", resolve);
			res.on("close", () => {
				this.onclose(req, res);
				reject(new Error("Connection closed before response was flushed"));
			});

			this.decorate(req, res);
			this.onconnect(req, res);

			if (regex.isOptions.test(method) === true && this.allowed(method, req.parsed.pathname) === false) {
				method = "GET"; // Changing an OPTIONS request to GET due to absent route
			}

			if (mregex.test(req.allow)) {
				result = this.routes(req.parsed.pathname, method);

				if (result.params !== void 0) {
					this.params(req, result.pos);
				}

				middleware = result.middleware[Symbol.iterator]();
				next();
			} else {
				last();
			}
		}).then(() => this.onfinish(req, res), err => {
			this.onerror(req, res, err);
			this.onfinish(req, res);
		});
	}

	routes (uri, method, override = false) {
		const key = this.hash(method + delimiter + uri),
			cached = override === false ? this.cache.get(key) : void 0;
		let result;

		if (cached !== void 0) {
			result = cached;
		} else {
			let middleware = [],
				pos = [],
				params = false;

			each([this.middleware.get(all.toUpperCase()), this.middleware.get(method)], map => {
				if (map !== void 0) {
					each(Array.from(map.keys()).filter(route => {
						let now = false,
							valid;

						if (regex.hasParam.test(route) === true && regex.braceLeft.test(route) === false && params === false) {
							params = true;
							now = true;

							each(route.replace(regex.slashEnd, "").replace(regex.slashEnd, "").split("/"), (i, idx) => {
								if (regex.isParam.test(i) === true) {
									pos.push([idx, i.replace(regex.isParam, "")]);
								}
							});

							route = route.replace(/\/:(\w*)/g, "/(.*)");
						}

						try {
							valid = new RegExp("^" + route + "$", "i").test(uri);
						} catch (e) {
							valid = new RegExp("^" + route.replace(/[-[\]\/{}()*+?.,\\^$|#]/g, "\\$&") + "$", "i").test(uri);
						}

						if (now === true && valid === false) {
							params = false;
							pos = [];
						}

						return valid;
					}), route => {
						middleware = middleware.concat(map.get(route));
					});
				}
			});

			result = {middleware: middleware, params: params, pos: pos};
			this.cache.set(key, result);
		}

		return result;
	}

	schemeless (arg) {
		return arg.replace(/^.*:\/\//, "");
	}

	use (...args) {
		let rpath = args[0],
			fn = args[1],
			method = args[2],
			mmethod;

		if (typeof rpath !== "string") {
			method = fn;
			fn = rpath;
			rpath = "/.*";
		}

		method = method !== void 0 ? method.toUpperCase() : "GET";

		if (typeof fn !== "function") {
			throw new TypeError("Invalid middleware");
		}

		if (new RegExp(all, "i").test(method) === false && methods.includes(method) === false) {
			throw new TypeError("Invalid HTTP method");
		}

		if (regex.isHead.test(method) === true) {
			throw new TypeError("Cannot set HEAD route, use GET");
		}

		if (this.middleware.has(method) === false) {
			this.middleware.set(method, new Map());
		}

		mmethod = this.middleware.get(method);

		if (mmethod.has(rpath) === false) {
			mmethod.set(rpath, []);
		}

		fn.hash = this.hash(fn.toString());
		mmethod.get(rpath).push(fn);

		return this;
	}
}

module.exports = Woodland;
<commit_msg>Adding `res.error()` & `res.redirect()` helpers<commit_after>"use strict";

const tcoerce = require("tiny-coerce"),
	lru = require("tiny-lru"),
	http = require("http"),
	parse = require("tiny-parse"),
	mmh3 = require("murmurhash3js").x86.hash32,
	all = "all",
	delimiter = ":",
	methods = http.METHODS,
	regex = {
		braceLeft: /\(/,
		hasGet: /GET/,
		hasParam: /\/:(\w*)/,
		isHead: /^HEAD$/,
		isParam: /^:/,
		isOptions: /^OPTIONS$/,
		slashEnd: /\/$/,
		slashStart: /^\//
	};

function clone (arg) {
	return JSON.parse(JSON.stringify(arg));
}

function each (arr, fn) {
	const nth = arr.length;
	let i = -1;

	while (++i < nth) {
		fn(arr[i], i);
	}

	return arr;
}

class Woodland {
	constructor (defaultHeaders, cacheSize, seed, coerce, http2) {
		this.blacklisted = new Set();
		this.cache = lru(cacheSize);
		this.coerce = coerce;
		this.http2 = http2;
		this.defaultHeaders = Object.keys(defaultHeaders).map(key => [key.toLowerCase(), defaultHeaders[key]]);
		this.permissions = lru(cacheSize);
		this.middleware = new Map();
		this.seed = seed;
	}

	allowed (method, uri, override = false) {
		return this.routes(uri, method, override).middleware.filter(i => this.blacklisted.has(i.hash) === false).length > 0;
	}

	allows (uri, override = false) {
		const key = uri;
		let result = override === false ? this.permissions.get(key) : void 0;

		if (override === true || result === void 0) {
			const list = new Set(methods.filter(i => this.allowed(i, uri, override)));

			if (list.has("GET")) {
				list.add("HEAD");
				list.add("OPTIONS");
			}

			result = Array.from(list).sort().join(", ");
			this.permissions.set(key, result);
		}

		return result;
	}

	blacklist (fn) {
		if (this.blacklisted.has(fn.hash) === false) {
			this.blacklisted.add(fn.hash);
		}

		return this;
	}

	decorate (req, res) {
		const parsed = parse(req, this.coerce),
			remotes = req.headers["x-forwarded-for"] !== void 0 ? req.headers["x-forwarded-for"].split(/\s*,\s*/g) : [req.connection.remoteAddress];

		if (this.http2) {
			res.tempHeaders = {};

			res.setHeader = (key, value) => {
				res.tempHeaders[key] = value;
			};

			res.send = (body, status = 200, headers = null) => {
				if (res.writable) {
					const output = {":status": status};

					each(Object.keys(res.tempHeaders), i => {
						output[i] = res.tempHeaders[i];
					});

					res.tempHeaders = {};

					if (headers !== null) {
						each(Object.keys(headers), i => {
							output[i] = headers[i];
						});
					}

					res.respond(output);
					res.end(body);
				}
			};
		} else {
			res.send = (body, status, headers) => obj.respond(req, res, body, status, headers);
		}

		res.error = (status, body, headers) => res.send(body, status, headers);
		res.redirect = (uri, perm = true) => res.send("", perm ? 301 : 302, {"location:": uri});

		// Express interop
		res.header = res.setHeader;
		res.locals = {};
		req.params = {};

		// Decorating useful hints
		req.body = "";
		req.ip = remotes[remotes.length - 1];
		req.parsed = parsed;
		req.query = parsed.query;
		req.host = req.parsed.hostname;
		req.allow = this.allows(req.parsed.pathname);
		req.cors = req.headers.origin !== void 0 && this.schemeless(req.headers.host) !== this.schemeless(req.headers.origin);

		// CORS handling
		if (req.cors === true) {
			let headers = req.headers["access-control-request-headers"];

			res.header("access-control-allow-origin", req.headers.origin);
			res.header("access-control-allow-credentials", "true");

			if (headers !== void 0) {
				res.header(regex.isOptions.test(req.method) ? "access-control-allow-headers" : "access-control-expose-headers", headers);
			}

			if (req.allow !== "") {
				res.header("access-control-allow-methods", req.allow);
			}
		}

		// Setting headers
		each(this.defaultHeaders, i => res.header(i[0], i[1]));

		if (req.allow !== "") {
			res.header("allow", req.allow);
		}
	}

	hash (arg) {
		return mmh3(arg, this.seed);
	}

	list (method = "get", type = "array") {
		let result;

		if (type === "array") {
			result = Array.from(this.middleware.get(method.toUpperCase()).keys());
		} else if (type === "object") {
			result = {};

			this.middleware.get(method.toUpperCase()).forEach((value, key) => {
				result[key] = value;
			});
		}

		return result;
	}

	normalize (arg) {
		const obj = clone(arg);

		obj.method = obj.headers[":method"];
		obj.connection = {remoteAddress: "0.0.0.0"};
		obj.url = obj.headers[":path"]; //`${obj.headers[":scheme"]}//${obj.headers[":authority"]}${obj.headers[":path"]}`;

		delete obj.headers[":authority"];
		delete obj.headers[":method"];
		delete obj.headers[":path"];
		delete obj.headers[":scheme"];

		return obj;
	}

	onclose () {}

	onconnect () {}

	onerror (req, res, err) {
		if (this.http2 === false) {
			const numeric = isNaN(err.message) === false,
				status = isNaN(res.statusCode) === false && res.statusCode >= 400 ? res.statusCode : numeric ? Number(err.message) : 500,
				output = numeric ? http.STATUS_CODES[status] : err.message;

			res.statusCode = status;
			res.writeHead(status, {"content-type": "text/plain", "content-length": Buffer.byteLength(output)});
			res.end(output);
		} else {
			const numeric = isNaN(err.message) === false,
				status = numeric ? Number(err.message) : 500,
				output = numeric ? http.STATUS_CODES[status] : err.message;

			res.send(output, status, {"content-type": "text/plain"});
		}
	}

	onfinish () {}

	params (...args) {
		if (this.http2 === false) {
			const [req, pos = []] = args,
				uri = req.parsed.path.replace(regex.slashEnd, "").replace(regex.slashEnd, "").split("/");

			each(pos, i => {
				req.params[i[1]] = tcoerce(uri[i[0]]);
			});
		}
	}

	route (...args) {
		const [req, res] = this.http2 === false ? args : [this.normalize({headers: args[1]}), args[0]];

		return new Promise((resolve, reject) => {
			let method = regex.isHead.test(req.method) ? "GET" : req.method,
				mregex = new RegExp(method),
				status = res.statusCode || 0,
				middleware, result;

			const last = err => {
				if (err === void 0) {
					reject(new Error(regex.hasGet.test(req.allow || "") ? 405 : 404));
				} else if (isNaN(status) === false && status >= 400) {
					reject(err);
				} else {
					reject(new Error(status >= 400 ? status : isNaN(err.message) === false ? err.message : http.STATUS_CODES[err.message || err] || 500));
				}
			};

			const next = err => {
				process.nextTick(() => {
					let iter = middleware.next(),
						arity = iter.done === false ? iter.value.length : 0;

					if (iter.done === false) {
						if (err !== void 0) {
							do {
								arity = iter.value.length;
							} while (arity < 4 && (iter = middleware.next()) && iter.done === false);
						}

						if (iter.done === false) {
							if (err !== void 0 && arity === 4) {
								try {
									iter.value(err, req, res, next);
								} catch (e) {
									next(e);
								}
							} else if (err === void 0 && arity <= 3) {
								try {
									iter.value(req, res, next);
								} catch (e) {
									next(e);
								}
							} else {
								next(); // Error handler in the middle
							}
						} else {
							last(err); // Unhandled error
						}
					} else {
						last(err); // Called next() in the last middleware
					}
				});
			};

			res.on("finish", resolve);
			res.on("close", () => {
				this.onclose(req, res);
				reject(new Error("Connection closed before response was flushed"));
			});

			this.decorate(req, res);
			this.onconnect(req, res);

			if (regex.isOptions.test(method) === true && this.allowed(method, req.parsed.pathname) === false) {
				method = "GET"; // Changing an OPTIONS request to GET due to absent route
			}

			if (mregex.test(req.allow)) {
				result = this.routes(req.parsed.pathname, method);

				if (result.params !== void 0) {
					this.params(req, result.pos);
				}

				middleware = result.middleware[Symbol.iterator]();
				next();
			} else {
				last();
			}
		}).then(() => this.onfinish(req, res), err => {
			this.onerror(req, res, err);
			this.onfinish(req, res);
		});
	}

	routes (uri, method, override = false) {
		const key = this.hash(method + delimiter + uri),
			cached = override === false ? this.cache.get(key) : void 0;
		let result;

		if (cached !== void 0) {
			result = cached;
		} else {
			let middleware = [],
				pos = [],
				params = false;

			each([this.middleware.get(all.toUpperCase()), this.middleware.get(method)], map => {
				if (map !== void 0) {
					each(Array.from(map.keys()).filter(route => {
						let now = false,
							valid;

						if (regex.hasParam.test(route) === true && regex.braceLeft.test(route) === false && params === false) {
							params = true;
							now = true;

							each(route.replace(regex.slashEnd, "").replace(regex.slashEnd, "").split("/"), (i, idx) => {
								if (regex.isParam.test(i) === true) {
									pos.push([idx, i.replace(regex.isParam, "")]);
								}
							});

							route = route.replace(/\/:(\w*)/g, "/(.*)");
						}

						try {
							valid = new RegExp("^" + route + "$", "i").test(uri);
						} catch (e) {
							valid = new RegExp("^" + route.replace(/[-[\]\/{}()*+?.,\\^$|#]/g, "\\$&") + "$", "i").test(uri);
						}

						if (now === true && valid === false) {
							params = false;
							pos = [];
						}

						return valid;
					}), route => {
						middleware = middleware.concat(map.get(route));
					});
				}
			});

			result = {middleware: middleware, params: params, pos: pos};
			this.cache.set(key, result);
		}

		return result;
	}

	schemeless (arg) {
		return arg.replace(/^.*:\/\//, "");
	}

	use (...args) {
		let rpath = args[0],
			fn = args[1],
			method = args[2],
			mmethod;

		if (typeof rpath !== "string") {
			method = fn;
			fn = rpath;
			rpath = "/.*";
		}

		method = method !== void 0 ? method.toUpperCase() : "GET";

		if (typeof fn !== "function") {
			throw new TypeError("Invalid middleware");
		}

		if (new RegExp(all, "i").test(method) === false && methods.includes(method) === false) {
			throw new TypeError("Invalid HTTP method");
		}

		if (regex.isHead.test(method) === true) {
			throw new TypeError("Cannot set HEAD route, use GET");
		}

		if (this.middleware.has(method) === false) {
			this.middleware.set(method, new Map());
		}

		mmethod = this.middleware.get(method);

		if (mmethod.has(rpath) === false) {
			mmethod.set(rpath, []);
		}

		fn.hash = this.hash(fn.toString());
		mmethod.get(rpath).push(fn);

		return this;
	}
}

module.exports = Woodland;
<|endoftext|>