<commit_before>/**
 * jQuery object
 * @external jQuery
 * @see {@link http://api.jquery.com/jQuery/}
 */

/**
 * Server response for filtering query for given representation.
 * @typedef {Object} ServerResponse
 * @property {FiltersData} filters
 * @property content - Content to be passed to provided data_handler
 */

/**
 * Filter-specific metadata allowing to recognise if a response id up-to-date,
 * to update widgets if dataset has changed and so on.
 * @typedef {Object} FiltersData
 * @property {boolean} checksum - Semaphore-like checksum of filters handled
 * @property {html} dynamic_widgets - Widgets applicable only to selected dataset
 * @property {string} query - Value of query as to be used in 'filters={{value}}' URL query string.
 * @property {string} expanded_query - Like query but including default filters values.
 */

/**
 * @class
 * @return {{init: init, value: get_value, load: load, apply: apply, on_update: on_update}}
 */
var AsyncFiltersHandler = function()
{
    var config;
    var form;
    var old_filters_query;
    var current_state_checksum;

    /**
     * Generate checksum string for given query string.
     * @param {string} query
     * @returns {string} checksum of given query string
     */
    function make_checksum(query)
    {
        return md5(query);
    }

    /**
     * Serialize a form into a GET-parameters list (for use in URLs),
     * additionally, a checksum parameter (md5) will be added.
     * @param {jQuery} $form - Form to be serialized.
     * @returns {string} Generated query string (parameters).
     */
    function serialize_form($form)
    {
        var filters_query = $form.serialize();
        var checksum = make_checksum(filters_query);

        filters_query += filters_query ? '&' : '?';

        filters_query += 'checksum=' + checksum;
        return filters_query
    }

    /**
     * Transform current URL (location.href) to include provided parameters.
     * @param {string} parameters - Query string to be included.
     * @returns {string} Current URL updated with provided filters query.
     */
    function make_query_url(parameters)
    {
        var location = window.location.href;
        var splitted = location.split('#');
        var hash = splitted.length > 0 ? splitted[1] : '';
        location = splitted[0].split('?')[0];

        if (parameters) {
            location += '?' + parameters;
        }

        if (hash) {
            location += '#' + hash;
        }
        return location;
    }

    /** Callback to update event which will be bound to the form. */
    function on_update(do_not_save)
    {
        // do not apply filters till all widget blockades are released
        if(form.find('.block').length !== 0) {
            return false
        }

        var filters_query = serialize_form(form);

        apply(filters_query, do_not_save);
    }

    /**
     * Serialize as much as (easily) possible inside given DOM structure.
     * Generally used to compare two HTML fragments containing similar but not identical forms.
     * If for both fragments the function returns the same, then then values of inputs are identical.
     * @param {jQuery} $dom_fragment - Part of HTML document to serialize.
     * @returns {string} Query string from serialized inputs inside given DOM fragment.
     */
    function serialize_fragment($dom_fragment)
    {
        return $dom_fragment.find('input,select').serialize()
    }

    /**
     * @param {FiltersData} data
     * @returns {boolean} is response up to date?
     */
    function is_response_actual(data)
    {
        return make_checksum(form.serialize()) === data.checksum;
    }

    /**
     * Prevents concurrency issue of accepting two or more consecutive
     * responses, which all pass "is_response_actual" test.
     * It prevents effect of duplicated visualisations as shown in #121 issue.
     * @param {FiltersData} data
     * @returns {boolean} is different with currently set value?
     */
    function does_response_differ_from_current_state(data)
    {
        return current_state_checksum != data.checksum
    }

    /**
     * Replace filters form with relevant (updated) content:
     * - set up dynamic widgets (widgets which change, depending on
     *   values of other filters, e.g. dataset-specific widgets: we
     *   do not want to filter by cancer type in ESP6500 dataset)
     * - correctly selected checkboxes / inputs
     *   (when restoring to the old state with History API,
     *   those has to be replaced accordingly to old state)
     * @param {FiltersData} data - server response defining current state
     * @param {boolean} from_future - Was the update called on "popstate" History API event?
     */
    function update_form_html(data, from_future)
    {
        var html = $.parseHTML(data.dynamic_widgets);

        if(from_future)
        {
            form.get(0).reset()
            form.deserialize(history.state.form)
            form.trigger('PotentialAffixChange');
        }

        var dynamic_widgets = $('.dynamic-widgets');

        // do not replace if it's not needed - so expanded lists stay expanded
        if(serialize_fragment(dynamic_widgets) !== serialize_fragment($(html)))
        {
            dynamic_widgets.html(html);
            dynamic_widgets.trigger('PotentialAffixChange');
        }
    }

    /**
     * Update an HTML <a> link, substituting '{{ filters }}' with given
     * filters string and cleaning up resultant URL from unused parameters.
     * @param {HTMLElement} element - <a> element to be updated; has to contain appropriate 'data-url-pattern'
     * @param {string} filters_string - query string to be used
     */
    function update_link(element, filters_string)
    {
        var $a = $(element);

        var pattern = decode_url_pattern($a.data('url-pattern'));
        var new_href = format(pattern, {filters: filters_string});

        // clean up the url from unused parameters
        new_href = new_href.replace(/(&|\?)(.*?)=(&|$)/, '');

        $a.attr('href', new_href);
    }

    /**
     * Handle response from.
     * @param {ServerResponse} data - server response defining current state
     * @param {boolean} from_future - Was the update called on "popstate" History API event?
     */
    function load(data, from_future)
    {
        var filters_data = data.filters;

        if (!(is_response_actual(filters_data) && does_response_differ_from_current_state(filters_data)) && !from_future)
        {
            console.log('Skipping not actual response');
            return
        }
        current_state_checksum = filters_data.checksum

        config.data_handler(data.content, filters_data);

        update_form_html(filters_data, from_future);

        if(config.links_to_update)
        {
            config.links_to_update.each(function(){
                update_link(this, filters_data.query)
            });
        }

        config.on_loading_end();
    }

    function update_history(query, replace)
    {
        var history_action = history.pushState;

        if (replace)
            history_action = history.replaceState;

        var state = {filters_query: query, form: form.serialize(), handler: 'filters'};

        history_action(state, '', make_query_url(query));
    }

    /**
     * Apply filters provided in query:
     *  - ask server for data for those filters,
     *  - change URL,
     *  - record changes with History API.
     * @param {string} filters_query - Query string as returned by {@see serialize_form}
     * @param {boolean} [do_not_save=false] - Should this modification be recorded in history?
     * @param {boolean} [from_future=false] - Was called on "popstate" History API event?
     */
    function apply(filters_query, do_not_save, from_future)
    {
        config.on_loading_start();
        current_state_checksum = null;

        $.ajax({
            url: config.endpoint_url,
            data: filters_query,
            success: function(data){

                var filters_query = ''

                if(data.filters.query)
                    filters_query += 'filters=' + data.filters.query

                load(data, from_future)

                update_history(filters_query, do_not_save)

            }
        });

        old_filters_query = filters_query;
    }

    /**
     * Retrieve currently set value of a filter of given name.
     *
     * Returns undefined if there is no selected value or if
     * the current value is default one and not explicitly set.
     */
    function get_value(filter_name)
    {
        var matched = form.serializeArray().filter(
            function(o){ return o.name === 'filter[' + filter_name + ']' }
        )
        if(matched.length !== 1)
            return undefined
        return matched[0].value
    }

    /**
     * Configuration object for AsyncFiltersHandler.
     * @typedef {Object} Config
     * @memberOf AsyncFiltersHandler
     * @property {jQuery} form
     * @property {function} data_handler
     * @property {function} on_loading_start
     * @property {function} on_loading_end
     * @property {jQuery} links_to_update
     * @property {string} endpoint_url - an URL of endpoint returning {@see ServerResponse}
     *  the endpoint should accept checksum, (and return in {@see FiltersData})
     */

    return {
        /**
         * Initialize AsyncFiltersHandler
         * @memberOf AsyncFiltersHandler
         * @param {Config} new_config
         */
        init: function(new_config)
        {
            config = new_config;
            form = config.form;
            form.on(
                'change',
                'select, input:not([type=text]):not(.programmatic)',
                function() { on_update() }
            );
            form.on(
                'input',
                'input[type=text]:not(.programmatic)',
                function() { on_update() }
            );
            old_filters_query = serialize_form(form);
            form.find('.save').hide()

            update_history(window.location.search.substring(1), true)
        },
        value: get_value,
        load: load,
        apply: apply,
        on_update: on_update
    };
};
<commit_msg>Reduce number of requests to server by waiting 200 ms for user to end typing.<commit_after>/**
 * jQuery object
 * @external jQuery
 * @see {@link http://api.jquery.com/jQuery/}
 */

/**
 * Server response for filtering query for given representation.
 * @typedef {Object} ServerResponse
 * @property {FiltersData} filters
 * @property content - Content to be passed to provided data_handler
 */

/**
 * Filter-specific metadata allowing to recognise if a response id up-to-date,
 * to update widgets if dataset has changed and so on.
 * @typedef {Object} FiltersData
 * @property {boolean} checksum - Semaphore-like checksum of filters handled
 * @property {html} dynamic_widgets - Widgets applicable only to selected dataset
 * @property {string} query - Value of query as to be used in 'filters={{value}}' URL query string.
 * @property {string} expanded_query - Like query but including default filters values.
 */

/**
 * @class
 * @return {{init: init, value: get_value, load: load, apply: apply, on_update: on_update}}
 */
var AsyncFiltersHandler = function()
{
    var config;
    var form;
    var current_state_checksum;

    /**
     * Generate checksum string for given query string.
     * @param {string} query
     * @returns {string} checksum of given query string
     */
    function make_checksum(query)
    {
        return md5(query);
    }

    /**
     * Serialize a form into a GET-parameters list (for use in URLs),
     * additionally, a checksum parameter (md5) will be added.
     * @param {jQuery} $form - Form to be serialized.
     * @returns {string} Generated query string (parameters).
     */
    function serialize_form($form)
    {
        var filters_query = $form.serialize();
        var checksum = make_checksum(filters_query);

        filters_query += filters_query ? '&' : '?';

        filters_query += 'checksum=' + checksum;
        return filters_query
    }

    /**
     * Transform current URL (location.href) to include provided parameters.
     * @param {string} parameters - Query string to be included.
     * @returns {string} Current URL updated with provided filters query.
     */
    function make_query_url(parameters)
    {
        var location = window.location.href;
        var splitted = location.split('#');
        var hash = splitted.length > 0 ? splitted[1] : '';
        location = splitted[0].split('?')[0];

        if (parameters) {
            location += '?' + parameters;
        }

        if (hash) {
            location += '#' + hash;
        }
        return location;
    }

    /** Callback to update event which will be bound to the form. */
    function on_update(do_not_save)
    {
        // do not apply filters till all widget blockades are released
        if(form.find('.block').length !== 0) {
            return false
        }

        var filters_query = serialize_form(form);

        apply(filters_query, do_not_save);
    }

    /**
     * Serialize as much as (easily) possible inside given DOM structure.
     * Generally used to compare two HTML fragments containing similar but not identical forms.
     * If for both fragments the function returns the same, then then values of inputs are identical.
     * @param {jQuery} $dom_fragment - Part of HTML document to serialize.
     * @returns {string} Query string from serialized inputs inside given DOM fragment.
     */
    function serialize_fragment($dom_fragment)
    {
        return $dom_fragment.find('input,select').serialize()
    }

    /**
     * @param {FiltersData} data
     * @returns {boolean} is response up to date?
     */
    function is_response_actual(data)
    {
        return make_checksum(form.serialize()) === data.checksum;
    }

    /**
     * Prevents concurrency issue of accepting two or more consecutive
     * responses, which all pass "is_response_actual" test.
     * It prevents effect of duplicated visualisations as shown in #121 issue.
     * @param {FiltersData} data
     * @returns {boolean} is different with currently set value?
     */
    function does_response_differ_from_current_state(data)
    {
        return current_state_checksum != data.checksum
    }

    /**
     * Replace filters form with relevant (updated) content:
     * - set up dynamic widgets (widgets which change, depending on
     *   values of other filters, e.g. dataset-specific widgets: we
     *   do not want to filter by cancer type in ESP6500 dataset)
     * - correctly selected checkboxes / inputs
     *   (when restoring to the old state with History API,
     *   those has to be replaced accordingly to old state)
     * @param {FiltersData} data - server response defining current state
     * @param {boolean} from_future - Was the update called on "popstate" History API event?
     */
    function update_form_html(data, from_future)
    {
        var html = $.parseHTML(data.dynamic_widgets);

        if(from_future)
        {
            form.get(0).reset()
            form.deserialize(history.state.form)
            form.trigger('PotentialAffixChange');
        }

        var dynamic_widgets = $('.dynamic-widgets');

        // do not replace if it's not needed - so expanded lists stay expanded
        if(serialize_fragment(dynamic_widgets) !== serialize_fragment($(html)))
        {
            dynamic_widgets.html(html);
            dynamic_widgets.trigger('PotentialAffixChange');
        }
    }

    /**
     * Update an HTML <a> link, substituting '{{ filters }}' with given
     * filters string and cleaning up resultant URL from unused parameters.
     * @param {HTMLElement} element - <a> element to be updated; has to contain appropriate 'data-url-pattern'
     * @param {string} filters_string - query string to be used
     */
    function update_link(element, filters_string)
    {
        var $a = $(element);

        var pattern = decode_url_pattern($a.data('url-pattern'));
        var new_href = format(pattern, {filters: filters_string});

        // clean up the url from unused parameters
        new_href = new_href.replace(/(&|\?)(.*?)=(&|$)/, '');

        $a.attr('href', new_href);
    }

    /**
     * Handle response from.
     * @param {ServerResponse} data - server response defining current state
     * @param {boolean} from_future - Was the update called on "popstate" History API event?
     */
    function load(data, from_future)
    {
        var filters_data = data.filters;

        if (!(is_response_actual(filters_data) && does_response_differ_from_current_state(filters_data)) && !from_future)
        {
            console.log('Skipping outdated response');
            return
        }
        current_state_checksum = filters_data.checksum

        config.data_handler(data.content, filters_data);

        update_form_html(filters_data, from_future);

        if(config.links_to_update)
        {
            config.links_to_update.each(function(){
                update_link(this, filters_data.query)
            });
        }

        config.on_loading_end();
    }

    function update_history(query, replace)
    {
        var history_action = history.pushState;

        if (replace)
            history_action = history.replaceState;

        var state = {filters_query: query, form: form.serialize(), handler: 'filters'};

        history_action(state, '', make_query_url(query));
    }

    /**
     * Apply filters provided in query:
     *  - ask server for data for those filters,
     *  - change URL,
     *  - record changes with History API.
     * @param {string} filters_query - Query string as returned by {@see serialize_form}
     * @param {boolean} [do_not_save=false] - Should this modification be recorded in history?
     * @param {boolean} [from_future=false] - Was called on "popstate" History API event?
     */
    function apply(filters_query, do_not_save, from_future)
    {
        config.on_loading_start();
        current_state_checksum = null;

        $.ajax({
            url: config.endpoint_url,
            data: filters_query,
            success: function(data){

                var filters_query = ''

                if(data.filters.query)
                    filters_query += 'filters=' + data.filters.query

                load(data, from_future)

                update_history(filters_query, do_not_save)

            }
        });
    }

    /**
     * Retrieve currently set value of a filter of given name.
     *
     * Returns undefined if there is no selected value or if
     * the current value is default one and not explicitly set.
     */
    function get_value(filter_name)
    {
        var matched = form.serializeArray().filter(
            function(o){ return o.name === 'filter[' + filter_name + ']' }
        )
        if(matched.length !== 1)
            return undefined
        return matched[0].value
    }

    /**
     * Configuration object for AsyncFiltersHandler.
     * @typedef {Object} Config
     * @memberOf AsyncFiltersHandler
     * @property {jQuery} form
     * @property {function} data_handler
     * @property {function} on_loading_start
     * @property {function} on_loading_end
     * @property {number} input_delay
     * @property {jQuery} links_to_update
     * @property {string} endpoint_url - an URL of endpoint returning {@see ServerResponse}
     *  the endpoint should accept checksum, (and return in {@see FiltersData})
     */

    return {
        /**
         * Initialize AsyncFiltersHandler
         * @memberOf AsyncFiltersHandler
         * @param {Config} new_config
         */
        init: function(new_config)
        {
            config = new_config;
            form = config.form;
            form.on(
                'change',
                'select, input:not([type=text]):not(.programmatic)',
                function() { on_update() }
            );

            var timer;

            form.on(
                'input',
                'input[type=text]:not(.programmatic)',
                function() {

                    if(timer)
                        window.clearTimeout(timer)

                    timer = window.setTimeout(
                        function(){
                            timer = null
                            on_update()
                        },
                        config.input_delay || 200
                    )
                }
            );

            form.find('.save').hide()

            update_history(window.location.search.substring(1), true)
        },
        value: get_value,
        load: load,
        apply: apply,
        on_update: on_update
    };
};
<|endoftext|>


<commit_before>'use strict';

/* Controllers */

angular.module('photoshare.controllers', ['photoshare.services'])
    .controller('AppCtrl', ['$scope',
                            '$location',
                            '$timeout',
                            'Authenticator',
                            'Alert',
                            function ($scope,
                                      $location,
                                      $timeout,
                                      Authenticator,
                                      Alert) {

            $scope.auth = Authenticator;
            $scope.alert = Alert;
            $scope.searchQuery = "";

            $scope.$watch('alert.message', function (newValue, oldValue) {
                if (newValue) {
                    $timeout(function () { Alert.dismiss(); }, 3000);
                }
            });

            $scope.logout = function () {
                $scope.auth.logout().then(function () {
                    $location.path("/list");
                });
            };

            /*
            $scope.$on("login", function (event, newUser) {
                $scope.auth.loggedIn = true;
                $scope.auth.currentUser = newUser;
            });
            */

            $scope.doSearch = function () {
                $location.path("/search/" + $scope.searchQuery);
                $scope.searchQuery = "";
            };
        }])

    .controller('ListCtrl', ['$scope',
                             '$location',
                             '$routeParams',
                             'Photo',
                             'pageSize',
                             function ($scope, $location, $routeParams, Photo, pageSize) {
            var page = 1,
                stopScrolling = false,
                q = $routeParams.q || "",
                ownerID = $routeParams.ownerID || "",
                ownerName = $routeParams.ownerName || "";

            $scope.photos = [];
            $scope.searchQuery = q;
            $scope.ownerName = ownerName;
            $scope.nextPage = function () {
                if (!stopScrolling) {
                    Photo.query({page: page, q: q, ownerID: ownerID}).$promise.then(function (photos) {
                        $scope.photos = $scope.photos.concat(photos);
                        if (photos.length < pageSize) {
                            stopScrolling = true;
                        }
                    });
                }
                page += 1;
            };

            $scope.getDetail = function (photo) {
                $location.path("/detail/" + photo.id);
            };

        }])

    .controller('DetailCtrl', ['$scope',
                               '$routeParams',
                               '$location',
                               'Photo',
                               'Authenticator',
                               'Alert',
                               function ($scope, $routeParams, $location, Photo, Authenticator, Alert) {
            $scope.photo = null;
            $scope.editTitle = false;
            Photo.get({id: $routeParams.id}).$promise.then(function (photo) {
                $scope.photo = photo;
                $scope.canDelete = Authenticator.canDelete($scope.photo);
                $scope.canEdit = Authenticator.canEdit($scope.photo);
            });
            $scope.deletePhoto = function () {
                $scope.photo.$delete(function () {
                    Alert.warning('Your photo has been deleted');
                    $location.path("/");
                });
            };
            $scope.showEditForm = function () {
                if ($scope.canEdit) {
                    $scope.editTitle = true;
                }
            };
            $scope.hideEditForm = function () {
                $scope.editTitle = false;
            };
            $scope.updateTitle = function () {
                Photo.update({id: $scope.photo.id,
                              title: $scope.photo.title}, function () {
                    $scope.editTitle = false;
                });
            };

        }])

    .controller('UploadCtrl', ['$scope',
                               '$location',
                               '$window',
                               'Authenticator',
                               'Alert',
                               'Photo', function ($scope, $location, $window, Authenticator, Alert, Photo) {
        if (!Authenticator.session.loggedIn) {
            Alert.danger("You must be logged in");
            $location.path("/login");
            return;
        }
        $scope.newPhoto = new Photo();
        $scope.upload = null;
        $scope.formDisabled = false;
        $scope.uploadPhoto = function () {
            $scope.formDisabled = true;
            $scope.newPhoto.$save(
                function () {
                    $scope.newPhoto = new Photo();
                    Alert.success('Your photo has been uploaded');
                    $location.path("/list");
                },
                function () {
                    $scope.formDisabled = false;
                }
            );
        };

    }])

    .controller('LoginCtrl', ['$scope',
                              '$location',
                              'Authenticator',
                              'Alert', function ($scope, $location, Authenticator, Alert) {
        $scope.loginCreds = new Authenticator.resource();
        $scope.login = function () {
            $scope.loginCreds.$save(function (result) {
                $scope.loginCreds = new Authenticator.resource();
                if (result.loggedIn) {
                    Authenticator.session = result;
                    Alert.success("Welcome back, " + Authenticator.session.name);
                    $location.path("/list");
                }
            });
        };
    }])

    .controller('SignupCtrl', ['$scope',
                               '$location',
                               'User',
                               'Authenticator',
                               'Alert', function ($scope, $location, User, Authenticator, Alert) {

        $scope.newUser = new User();
        $scope.signup = function () {
            $scope.newUser.$save(function (result) {
                Authenticator.session = result;
                $scope.newUser = new User();
                Alert.success("Welcome, " + result.name);
                $location.path("/list");
            });
        };
    }]);
<commit_msg>Fix authentication<commit_after>'use strict';

/* Controllers */

angular.module('photoshare.controllers', ['photoshare.services'])
    .controller('AppCtrl', ['$scope',
                            '$location',
                            '$timeout',
                            'Authenticator',
                            'Alert',
                            function ($scope,
                                      $location,
                                      $timeout,
                                      Authenticator,
                                      Alert) {

            $scope.auth = Authenticator;
            $scope.alert = Alert;
            $scope.searchQuery = "";

            $scope.auth.init();

            $scope.$watch('alert.message', function (newValue, oldValue) {
                if (newValue) {
                    $timeout(function () { Alert.dismiss(); }, 3000);
                }
            });

            $scope.logout = function () {
                $scope.auth.logout().then(function () {
                    $location.path("/list");
                });
            };

            $scope.doSearch = function () {
                $location.path("/search/" + $scope.searchQuery);
                $scope.searchQuery = "";
            };
        }])

    .controller('ListCtrl', ['$scope',
                             '$location',
                             '$routeParams',
                             'Photo',
                             'pageSize',
                             function ($scope, $location, $routeParams, Photo, pageSize) {
            var page = 1,
                stopScrolling = false,
                q = $routeParams.q || "",
                ownerID = $routeParams.ownerID || "",
                ownerName = $routeParams.ownerName || "";

            $scope.photos = [];
            $scope.searchQuery = q;
            $scope.ownerName = ownerName;
            $scope.nextPage = function () {
                if (!stopScrolling) {
                    Photo.query({page: page, q: q, ownerID: ownerID}).$promise.then(function (photos) {
                        $scope.photos = $scope.photos.concat(photos);
                        if (photos.length < pageSize) {
                            stopScrolling = true;
                        }
                    });
                }
                page += 1;
            };

            $scope.getDetail = function (photo) {
                $location.path("/detail/" + photo.id);
            };

        }])

    .controller('DetailCtrl', ['$scope',
                               '$routeParams',
                               '$location',
                               'Photo',
                               'Authenticator',
                               'Alert',
                               function ($scope, $routeParams, $location, Photo, Authenticator, Alert) {
            $scope.photo = null;
            $scope.editTitle = false;
            Photo.get({id: $routeParams.id}).$promise.then(function (photo) {
                $scope.photo = photo;
                $scope.canDelete = Authenticator.canDelete($scope.photo);
                $scope.canEdit = Authenticator.canEdit($scope.photo);
            });
            $scope.deletePhoto = function () {
                $scope.photo.$delete(function () {
                    Alert.warning('Your photo has been deleted');
                    $location.path("/");
                });
            };
            $scope.showEditForm = function () {
                if ($scope.canEdit) {
                    $scope.editTitle = true;
                }
            };
            $scope.hideEditForm = function () {
                $scope.editTitle = false;
            };
            $scope.updateTitle = function () {
                Photo.update({id: $scope.photo.id,
                              title: $scope.photo.title}, function () {
                    $scope.editTitle = false;
                });
            };

        }])

    .controller('UploadCtrl', ['$scope',
                               '$location',
                               '$window',
                               'Authenticator',
                               'Alert',
                               'Photo', function ($scope, $location, $window, Authenticator, Alert, Photo) {
        if (!Authenticator.session.loggedIn) {
            Alert.danger("You must be logged in");
            $location.path("/login");
            return;
        }
        $scope.newPhoto = new Photo();
        $scope.upload = null;
        $scope.formDisabled = false;
        $scope.uploadPhoto = function () {
            $scope.formDisabled = true;
            $scope.newPhoto.$save(
                function () {
                    $scope.newPhoto = new Photo();
                    Alert.success('Your photo has been uploaded');
                    $location.path("/list");
                },
                function () {
                    $scope.formDisabled = false;
                }
            );
        };

    }])

    .controller('LoginCtrl', ['$scope',
                              '$location',
                              'Authenticator',
                              'Alert', function ($scope, $location, Authenticator, Alert) {
        $scope.loginCreds = new Authenticator.resource();
        $scope.login = function () {
            $scope.loginCreds.$save(function (result) {
                $scope.loginCreds = new Authenticator.resource();
                if (result.loggedIn) {
                    Authenticator.session = result;
                    Alert.success("Welcome back, " + Authenticator.session.name);
                    $location.path("/list");
                }
            });
        };
    }])

    .controller('SignupCtrl', ['$scope',
                               '$location',
                               'User',
                               'Authenticator',
                               'Alert', function ($scope, $location, User, Authenticator, Alert) {

        $scope.newUser = new User();
        $scope.signup = function () {
            $scope.newUser.$save(function (result) {
                Authenticator.session = result;
                $scope.newUser = new User();
                Alert.success("Welcome, " + result.name);
                $location.path("/list");
            });
        };
    }]);
<|endoftext|>


<commit_before>"use strict";

var WEBATHENA_HOST = "https://webathena.mit.edu";

// Socket proxy errors.
var ERR_BAD_FORMAT = 4000;
var ERR_BAD_PARAMS = 4001;
var ERR_ALREADY_INITIALIZED = 4002;
var ERR_FORBIDDEN_ENDPOINT = 4003;
var ERR_SOCKET_ERROR = 4004;
var ERR_UNINITIALIZED = 4005;
var ERR_BAD_MESSAGE_TYPE = 4006;

var REMCTL_PORT = 4373;

var TOKEN_NOOP = 0x01;
var TOKEN_CONTEXT = 0x02;
var TOKEN_DATA = 0x04;
var TOKEN_CONTEXT_NEXT = 0x10;
var TOKEN_PROTOCOL = 0x40;

var MESSAGE_COMMAND = 0x01;
var MESSAGE_QUIT = 0x02;
var MESSAGE_OUTPUT = 0x03;
var MESSAGE_STATUS = 0x04;
var MESSAGE_ERROR = 0x05;
var MESSAGE_VERSION = 0x06;
var MESSAGE_NOOP = 0x07;

var ERROR_INTERNAL           = 1;
var ERROR_BAD_TOKEN          = 2;
var ERROR_UNKNOWN_MESSAGE    = 3;
var ERROR_BAD_COMMAND        = 4;
var ERROR_UNKNOWN_COMMAND    = 5;
var ERROR_ACCESS             = 6;
var ERROR_TOOMANY_ARGS       = 7;
var ERROR_TOOMUCH_DATA       = 8;
var ERROR_UNEXPECTED_MESSAGE = 9;

var MAX_TOKEN_SIZE = 1048576;
var MAX_WRAP_SIZE = 65536;

function RemctlError(code, message) {
    this.code = code;
    this.message = message;
}
RemctlError.prototype.toString = function() {
    return this.message;
};

function RemctlSocket(proxy, host, port) {
    if (port === undefined) port = REMCTL_PORT;

    // Default callbacks to no-ops.
    this.onpacket = function() { };

    // Various deferred's to get a nicer API for when the socket is
    // open or closed.
    this.deferredReady = Q.defer();
    this.deferredEnd = Q.defer();

    // True if buffer is waiting on a header, rather than data.
    this.pendingHeader = true;
    // If pendingHeader is false, the flags for the message we're reading.
    this.flags = -1;
    // The buffer for our current stage.
    this.buffer = new Uint8Array(5);
    // How far we are into the buffer.
    this.bufferPos = 0;

    this.socket = new SockJS(proxy);
    this.socket.addEventListener('open', function() {
        this.socket.send(JSON.stringify({
            type: 'init',
            host: host,
            port: port
        }));
    }.bind(this));
    this.socket.addEventListener('message', function(ev) {
        try {
            var msg = JSON.parse(ev.data);
        } catch (err) {
            this.disconnect(new Error('Bad server message format'));
            throw err;
        }
        if (msg.type === 'ready') {
            this.deferredReady.resolve(null);
        } else if (msg.type === 'data') {
            var b64 = msg.data;
            // We got more data. Copy what we can into the current buffer.
            var data = arrayutils.fromBase64(b64);
            while (data.length > 0) {
                var count = Math.min(this.buffer.length - this.bufferPos,
                                     data.length);
                // Copy data into the current buffer.
                this.buffer.set(data.subarray(0, count), this.bufferPos);
                this.bufferPos += count;
                data = data.subarray(count);

                // See if we've completed a buffer.
                if (this.bufferPos >= this.buffer.length) {
                    // We've either just gotten a 5-byte header or
                    // finished some data.
                    if (this.pendingHeader) {
                        var dataview = new DataView(this.buffer.buffer);
                        var len = dataview.getUint32(1);
                        if (len > MAX_TOKEN_SIZE) {
                            // Too long. Disconnect and error.
                            this.disconnect(new Error("Packet too long."));
                            return;
                        }
                        this.pendingHeader = false;
                        this.flags = dataview.getUint8(0);
                        this.buffer = new Uint8Array(len);
                        this.bufferPos = 0;
                    } else {
                        this.onpacket(this.flags, this.buffer);
                        this.pendingHeader = true;
                        this.buffer = new Uint8Array(5);
                        this.bufferPos = 0;
                    }
                }
            }
        } else {
            console.log('Unexpected message', msg);
        }
    }.bind(this));
    this.socket.addEventListener('close', function(ev) {
        console.log(this);
        if (ev.code === 1000) {
            this.disconnect();
        } else {
            // TODO(davidben): Pass the exit code along too.
            this.disconnect(new Error(ev.reason));
        }
    }.bind(this));
}
RemctlSocket.prototype.sendPacket = function(flags, data) {
    // Prepend the header.
    data = arrayutils.asUint8Array(data);
    var buf = new Uint8Array(1 + 4 + data.length);
    buf[0] = flags;
    new DataView(buf.buffer).setUint32(1, data.length);
    buf.set(data, 5);
    // And write.
    this.socket.send(JSON.stringify({
        type: 'write',
        data: arrayutils.toBase64(buf)
    }));
};
RemctlSocket.prototype.ready = function() {
    return this.deferredReady.promise;
};
RemctlSocket.prototype.end = function() {
    return this.deferredEnd.promise;
};
RemctlSocket.prototype.disconnect = function(reason) {
    if (this.deferredEnd.promise.isPending()) {
        if (this.deferredReady.promise.isPending())
            this.deferredReady.reject(reason);
        this.deferredEnd.resolve(reason);
        this.socket.close();
        this.socket = null;
    }
};

function RemctlSession(proxy, peer, credential, host, port) {
    this.deferredReady = Q.defer();

    // State for the current command.
    this.deferredStatus = null;
    this.onOutput = null;

    this.context = new gss.Context(peer, gss.KRB5_MECHANISM, credential, {
        mutualAuthentication: true,
        confidentiality: true,
        integrity: true,
        sequence: true,
        replayDetection: true
    });

    this.socket = new RemctlSocket(proxy, host, port);

    this.socket.ready().then(function() {
        this.socket.sendPacket(TOKEN_NOOP | TOKEN_CONTEXT_NEXT | TOKEN_PROTOCOL,
                               new Uint8Array(0));
        this._processAuthToken();
    }.bind(this), function(err) {
        this.deferredReady.reject(err);
    }.bind(this)).done();

    this.socket.end().then(function() {
        // If we haven't completed the context yet, we were never
        // ready. Make it throw.
        if (this.deferredReady.promise.isPending())
            this.deferredReady.reject(new Error("Disconnected"));
    }.bind(this)).done();

    this.socket.onpacket = function(flags, data) {
        if (this.context.isEstablished()) {
            if (flags !== (TOKEN_DATA|TOKEN_PROTOCOL)) {
                this.disconnect(new Error("Bad flags"));
                return;
            }
            var unwrapped = this.context.unwrap(data).message;
            if (unwrapped.length < 2) {
                this.disconnect(new Error("Bad message length"));
                return;
            }
            var version = unwrapped[0];
            var type = unwrapped[1];
            var data = unwrapped.subarray(2);
            this._handleMessage(version, type, data);
        } else {
            if (flags !== (TOKEN_CONTEXT|TOKEN_PROTOCOL)) {
                this.disconnect(new Error("Bad flags"));
                return;
            }
            this._processAuthToken(data);
        }
    }.bind(this);
}
RemctlSession.prototype._processAuthToken = function(token) {
    try {
        var resp = this.context.initSecContext(token);
        if (resp) {
            this.socket.sendPacket(TOKEN_CONTEXT|TOKEN_PROTOCOL, resp);
        }
        if (this.context.isEstablished()) {
            this.deferredReady.resolve(null);
        }
    } catch (e) {
        // I don't think it's possible for this to fail, but things
        // might have been reordered?
        if (this.deferredReady.promise.isPending())
            this.deferredReady.reject(e);
        // GSS error. Disconnect.
        this.socket.disconnect();
    }
};
RemctlSession.prototype._handleMessage = function(version, type, data) {
    // We should only receive messages when there is a pending
    // command.
    if (!this.deferredStatus) {
        this.disconnect(new Error("Unexpected message"));
        return;
    }
    if (type === MESSAGE_OUTPUT) {
        if (this.onOutput) {
            var dataview = new DataView(data.buffer,
                                        data.byteOffset,
                                        data.byteLength);
            var stream = dataview.getUint8(0);
            // What's the point of this? Whatever. We'll pull out that
            // length and check it.
            var length = dataview.getUint32(1);
            if (5 + length > data.length) {
                this.disconnect(new Error("Bad message"));
                return;
            }
            var output = data.subarray(5, 5 + length);
            this.onOutput(stream, output);
        }
    } else if (type === MESSAGE_STATUS) {
        var status = data[0];
        this.deferredStatus.resolve(status);
        this.deferredStatus = null;
        this.onOutput = null;
    } else if (type === MESSAGE_ERROR) {
        var dataview = new DataView(data.buffer,
                                    data.byteOffset,
                                    data.byteLength);
        var code = dataview.getUint32(0);
        // What's the point of this? Whatever. We'll pull out that
        // length and check it.
        var length = dataview.getUint32(4);
        if (8 + length > data.length) {
            this.disconnect(new Error("Bad message"));
            return;
        }
        var message = data.subarray(8, 8 + length);
        this.deferredStatus.reject(
            new RemctlError(code, arrayutils.toString(message)));
        this.deferredStatus = null;
        this.onOutput = null;
    } else if (type === MESSAGE_VERSION) {
        // TODO: If we handle MESSAGE_NOOP, we'll care about this.
    } else {
        this.disconnect(new Error("Unknown message type " + type));
    }
};
RemctlSession.prototype.ready = function() {
    return this.deferredReady.promise;
};
RemctlSession.prototype.end = function() {
    return this.socket.end();
};
RemctlSession.prototype.sendMessage = function(data) {
    // Meh. Probably could pass the version/type and have it assemble
    // this for you.
    this.socket.sendPacket(TOKEN_DATA | TOKEN_PROTOCOL,
                           this.context.wrap(data, true));
};
RemctlSession.prototype.disconnect = function(reason) {
    if (this.socket) {
        // If we got a disconnect mid-command, that's an error.
        if (this.deferredStatus)
            this.deferredStatus.reject(reason);
        this.socket.disconnect(reason);
        this.socket = null;
    }
};
RemctlSession.prototype.quit = function() {
    this.sendMessage(new Uint8Array([2, MESSAGE_QUIT]));
};
RemctlSession.prototype.command = function(args, onOutput, keepAlive) {
    // Only one command at a time. (Can you pipeline? Meh.)
    if (this.deferredStatus) {
        return Q.reject(new Error("remctl session is busy"));
    }

    this.onOutput = onOutput;
    this.deferredStatus = Q.defer();

    // Most commands are small, so figure out how much we need instead
    // of always allocating MAX_WRAP_SIZE.
    var len = 4 + 4;
    for (var i = 0; i < args.length; i++) {
        len += (4 + args[i].length);
    }
    var firstSend = true;
    var buffer = new Uint8Array(Math.min(len, MAX_WRAP_SIZE));
    // Reserve two bytes for the keep-alive and continue flags, in
    // addition to the message header.
    var offset = 4;

    // Various functions for buffering and sending buffers out when full.
    var flushBuffer = function(finish) {
        buffer[0] = 2;  // protocol version
        buffer[1] = MESSAGE_COMMAND;
        buffer[2] = keepAlive ? 1 : 0;
        if (firstSend)
            buffer[3] = finish ? 0 : 1;
        else
            buffer[3] = finish ? 3 : 2;
        this.sendMessage(buffer.subarray(0, offset));
        // Reserve two bytes for the keep-alive and continue flags, in
        // addition to the message header.
        offset = 4;
        firstSend = false;
    }.bind(this);
    var appendBytes = function(bytes) {
        while (bytes.length > 0) {
            // Flush buffer if full.
            if (offset === buffer.length)
                flushBuffer(false);
            // Append what we can.
            var len = Math.min(buffer.length - offset, bytes.length);
            buffer.set(bytes.subarray(0, len), offset);
            offset += len;
            bytes = bytes.subarray(len);
        }
    };
    var appendUint32 = function(val) {
        var buf = new Uint8Array(4);
        new DataView(buf.buffer).setUint32(0, val);
        appendBytes(buf);
    };

    // Phew. All that's out of the way. Now format the message.
    appendUint32(args.length);
    for (var i = 0; i < args.length; i++) {
        var arg = arrayutils.fromString(args[i]);
        appendUint32(arg.length);
        appendBytes(arg);
    }
    flushBuffer(true);

    return this.deferredStatus.promise;
};
<commit_msg>Update remctl.js from upstream.<commit_after>"use strict";

// Socket proxy errors.
var ERR_BAD_FORMAT = 4000;
var ERR_BAD_PARAMS = 4001;
var ERR_ALREADY_INITIALIZED = 4002;
var ERR_FORBIDDEN_ENDPOINT = 4003;
var ERR_SOCKET_ERROR = 4004;
var ERR_UNINITIALIZED = 4005;
var ERR_BAD_MESSAGE_TYPE = 4006;

var REMCTL_PORT = 4373;

var TOKEN_NOOP = 0x01;
var TOKEN_CONTEXT = 0x02;
var TOKEN_DATA = 0x04;
var TOKEN_CONTEXT_NEXT = 0x10;
var TOKEN_PROTOCOL = 0x40;

var MESSAGE_COMMAND = 0x01;
var MESSAGE_QUIT = 0x02;
var MESSAGE_OUTPUT = 0x03;
var MESSAGE_STATUS = 0x04;
var MESSAGE_ERROR = 0x05;
var MESSAGE_VERSION = 0x06;
var MESSAGE_NOOP = 0x07;

var ERROR_INTERNAL           = 1;
var ERROR_BAD_TOKEN          = 2;
var ERROR_UNKNOWN_MESSAGE    = 3;
var ERROR_BAD_COMMAND        = 4;
var ERROR_UNKNOWN_COMMAND    = 5;
var ERROR_ACCESS             = 6;
var ERROR_TOOMANY_ARGS       = 7;
var ERROR_TOOMUCH_DATA       = 8;
var ERROR_UNEXPECTED_MESSAGE = 9;

var MAX_TOKEN_SIZE = 1048576;
var MAX_WRAP_SIZE = 65536;

function RemctlError(code, message) {
    this.code = code;
    this.message = message;
}
RemctlError.prototype.toString = function() {
    return this.message;
};

function RemctlSocket(proxy, host, port) {
    if (port === undefined) port = REMCTL_PORT;

    // Default callbacks to no-ops.
    this.onpacket = function() { };

    // Various deferred's to get a nicer API for when the socket is
    // open or closed.
    this.deferredReady = Q.defer();
    this.deferredEnd = Q.defer();

    // True if buffer is waiting on a header, rather than data.
    this.pendingHeader = true;
    // If pendingHeader is false, the flags for the message we're reading.
    this.flags = -1;
    // The buffer for our current stage.
    this.buffer = new Uint8Array(5);
    // How far we are into the buffer.
    this.bufferPos = 0;

    this.socket = new SockJS(proxy);
    this.socket.addEventListener('open', function() {
        this.socket.send(JSON.stringify({
            type: 'init',
            host: host,
            port: port
        }));
    }.bind(this));
    this.socket.addEventListener('message', function(ev) {
        try {
            var msg = JSON.parse(ev.data);
        } catch (err) {
            this.disconnect(new Error('Bad server message format'));
            throw err;
        }
        if (msg.type === 'ready') {
            this.deferredReady.resolve(null);
        } else if (msg.type === 'data') {
            var b64 = msg.data;
            // We got more data. Copy what we can into the current buffer.
            var data = arrayutils.fromBase64(b64);
            while (data.length > 0) {
                var count = Math.min(this.buffer.length - this.bufferPos,
                                     data.length);
                // Copy data into the current buffer.
                this.buffer.set(data.subarray(0, count), this.bufferPos);
                this.bufferPos += count;
                data = data.subarray(count);

                // See if we've completed a buffer.
                if (this.bufferPos >= this.buffer.length) {
                    // We've either just gotten a 5-byte header or
                    // finished some data.
                    if (this.pendingHeader) {
                        var dataview = new DataView(this.buffer.buffer);
                        var len = dataview.getUint32(1);
                        if (len > MAX_TOKEN_SIZE) {
                            // Too long. Disconnect and error.
                            this.disconnect(new Error("Packet too long."));
                            return;
                        }
                        this.pendingHeader = false;
                        this.flags = dataview.getUint8(0);
                        this.buffer = new Uint8Array(len);
                        this.bufferPos = 0;
                    } else {
                        this.onpacket(this.flags, this.buffer);
                        this.pendingHeader = true;
                        this.buffer = new Uint8Array(5);
                        this.bufferPos = 0;
                    }
                }
            }
        } else {
            if (window.console && console.log)
                console.log('Unexpected message', msg);
        }
    }.bind(this));
    this.socket.addEventListener('close', function(ev) {
        if (ev.code === 1000) {
            this.disconnect();
        } else {
            // TODO(davidben): Pass the exit code along too.
            this.disconnect(new Error(ev.reason));
        }
    }.bind(this));
}
RemctlSocket.prototype.sendPacket = function(flags, data) {
    // Prepend the header.
    data = arrayutils.asUint8Array(data);
    var buf = new Uint8Array(1 + 4 + data.length);
    buf[0] = flags;
    new DataView(buf.buffer).setUint32(1, data.length);
    buf.set(data, 5);
    // And write.
    this.socket.send(JSON.stringify({
        type: 'write',
        data: arrayutils.toBase64(buf)
    }));
};
RemctlSocket.prototype.ready = function() {
    return this.deferredReady.promise;
};
RemctlSocket.prototype.end = function() {
    return this.deferredEnd.promise;
};
RemctlSocket.prototype.disconnect = function(reason) {
    if (this.deferredEnd.promise.isPending()) {
        if (this.deferredReady.promise.isPending())
            this.deferredReady.reject(reason);
        this.deferredEnd.resolve(reason);
        this.socket.close();
        this.socket = null;
    }
};

function RemctlSession(proxy, peer, credential, host, port) {
    this.deferredReady = Q.defer();

    // State for the current command.
    this.deferredStatus = null;
    this.onOutput = null;

    this.context = new gss.Context(peer, gss.KRB5_MECHANISM, credential, {
        mutualAuthentication: true,
        confidentiality: true,
        integrity: true,
        sequence: true,
        replayDetection: true
    });

    this.socket = new RemctlSocket(proxy, host, port);

    this.socket.ready().then(function() {
        this.socket.sendPacket(TOKEN_NOOP | TOKEN_CONTEXT_NEXT | TOKEN_PROTOCOL,
                               new Uint8Array(0));
        this._processAuthToken();
    }.bind(this), function(err) {
        this.deferredReady.reject(err);
    }.bind(this)).done();

    this.socket.end().then(function() {
        // If we haven't completed the context yet, we were never
        // ready. Make it throw.
        if (this.deferredReady.promise.isPending())
            this.deferredReady.reject(new Error("Disconnected"));
    }.bind(this)).done();

    this.socket.onpacket = function(flags, data) {
        if (this.context.isEstablished()) {
            if (flags !== (TOKEN_DATA|TOKEN_PROTOCOL)) {
                this.disconnect(new Error("Bad flags"));
                return;
            }
            var unwrapped = this.context.unwrap(data).message;
            if (unwrapped.length < 2) {
                this.disconnect(new Error("Bad message length"));
                return;
            }
            var version = unwrapped[0];
            var type = unwrapped[1];
            var data = unwrapped.subarray(2);
            this._handleMessage(version, type, data);
        } else {
            if (flags !== (TOKEN_CONTEXT|TOKEN_PROTOCOL)) {
                this.disconnect(new Error("Bad flags"));
                return;
            }
            this._processAuthToken(data);
        }
    }.bind(this);
}
RemctlSession.prototype._processAuthToken = function(token) {
    try {
        var resp = this.context.initSecContext(token);
        if (resp) {
            this.socket.sendPacket(TOKEN_CONTEXT|TOKEN_PROTOCOL, resp);
        }
        if (this.context.isEstablished()) {
            this.deferredReady.resolve(null);
        }
    } catch (e) {
        // I don't think it's possible for this to fail, but things
        // might have been reordered?
        if (this.deferredReady.promise.isPending())
            this.deferredReady.reject(e);
        // GSS error. Disconnect.
        this.socket.disconnect();
    }
};
RemctlSession.prototype._handleMessage = function(version, type, data) {
    // We should only receive messages when there is a pending
    // command.
    if (!this.deferredStatus) {
        this.disconnect(new Error("Unexpected message"));
        return;
    }
    if (type === MESSAGE_OUTPUT) {
        if (this.onOutput) {
            var dataview = new DataView(data.buffer,
                                        data.byteOffset,
                                        data.byteLength);
            var stream = dataview.getUint8(0);
            // What's the point of this? Whatever. We'll pull out that
            // length and check it.
            var length = dataview.getUint32(1);
            if (5 + length > data.length) {
                this.disconnect(new Error("Bad message"));
                return;
            }
            var output = data.subarray(5, 5 + length);
            this.onOutput(stream, output);
        }
    } else if (type === MESSAGE_STATUS) {
        var status = data[0];
        this.deferredStatus.resolve(status);
        this.deferredStatus = null;
        this.onOutput = null;
    } else if (type === MESSAGE_ERROR) {
        var dataview = new DataView(data.buffer,
                                    data.byteOffset,
                                    data.byteLength);
        var code = dataview.getUint32(0);
        // What's the point of this? Whatever. We'll pull out that
        // length and check it.
        var length = dataview.getUint32(4);
        if (8 + length > data.length) {
            this.disconnect(new Error("Bad message"));
            return;
        }
        var message = data.subarray(8, 8 + length);
        this.deferredStatus.reject(
            new RemctlError(code, arrayutils.toString(message)));
        this.deferredStatus = null;
        this.onOutput = null;
    } else if (type === MESSAGE_VERSION) {
        // TODO: If we handle MESSAGE_NOOP, we'll care about this.
    } else {
        this.disconnect(new Error("Unknown message type " + type));
    }
};
RemctlSession.prototype.ready = function() {
    return this.deferredReady.promise;
};
RemctlSession.prototype.end = function() {
    return this.socket.end();
};
RemctlSession.prototype.sendMessage = function(data) {
    // Meh. Probably could pass the version/type and have it assemble
    // this for you.
    this.socket.sendPacket(TOKEN_DATA | TOKEN_PROTOCOL,
                           this.context.wrap(data, true));
};
RemctlSession.prototype.disconnect = function(reason) {
    if (this.socket) {
        // If we got a disconnect mid-command, that's an error.
        if (this.deferredStatus)
            this.deferredStatus.reject(reason);
        this.socket.disconnect(reason);
        this.socket = null;
    }
};
RemctlSession.prototype.quit = function() {
    this.sendMessage(new Uint8Array([2, MESSAGE_QUIT]));
};
RemctlSession.prototype.command = function(args, onOutput, keepAlive) {
    // Only one command at a time. (Can you pipeline? Meh.)
    if (this.deferredStatus) {
        return Q.reject(new Error("remctl session is busy"));
    }

    this.onOutput = onOutput;
    this.deferredStatus = Q.defer();

    // Most commands are small, so figure out how much we need instead
    // of always allocating MAX_WRAP_SIZE.
    var len = 4 + 4;
    for (var i = 0; i < args.length; i++) {
        len += (4 + args[i].length);
    }
    var firstSend = true;
    var buffer = new Uint8Array(Math.min(len, MAX_WRAP_SIZE));
    // Reserve two bytes for the keep-alive and continue flags, in
    // addition to the message header.
    var offset = 4;

    // Various functions for buffering and sending buffers out when full.
    var flushBuffer = function(finish) {
        buffer[0] = 2;  // protocol version
        buffer[1] = MESSAGE_COMMAND;
        buffer[2] = keepAlive ? 1 : 0;
        if (firstSend)
            buffer[3] = finish ? 0 : 1;
        else
            buffer[3] = finish ? 3 : 2;
        this.sendMessage(buffer.subarray(0, offset));
        // Reserve two bytes for the keep-alive and continue flags, in
        // addition to the message header.
        offset = 4;
        firstSend = false;
    }.bind(this);
    var appendBytes = function(bytes) {
        while (bytes.length > 0) {
            // Flush buffer if full.
            if (offset === buffer.length)
                flushBuffer(false);
            // Append what we can.
            var len = Math.min(buffer.length - offset, bytes.length);
            buffer.set(bytes.subarray(0, len), offset);
            offset += len;
            bytes = bytes.subarray(len);
        }
    };
    var appendUint32 = function(val) {
        var buf = new Uint8Array(4);
        new DataView(buf.buffer).setUint32(0, val);
        appendBytes(buf);
    };

    // Phew. All that's out of the way. Now format the message.
    appendUint32(args.length);
    for (var i = 0; i < args.length; i++) {
        var arg = arrayutils.fromString(args[i]);
        appendUint32(arg.length);
        appendBytes(arg);
    }
    flushBuffer(true);

    return this.deferredStatus.promise;
};
<|endoftext|>


<commit_before>// ==========================================================================
// Project:   Ember - JavaScript Application Framework
// Copyright: ©2006-2011 Strobe Inc. and contributors.
//            Portions ©2008-2011 Apple Inc. All rights reserved.
// License:   Licensed under MIT license (see license.js)
// ==========================================================================

var get = Ember.get, set = Ember.set;

/**
  @class

  Ember.RenderBuffer gathers information regarding the a view and generates the
  final representation. Ember.RenderBuffer will generate HTML which can be pushed
  to the DOM.

  @extends Ember.Object
*/
Ember.RenderBuffer = function(tagName) {
  return Ember._RenderBuffer.create({ elementTag: tagName });
};

Ember._RenderBuffer = Ember.Object.extend(
/** @scope Ember.RenderBuffer.prototype */ {

  /**
    Array of class-names which will be applied in the class="" attribute

    You should not maintain this array yourself, rather, you should use
    the addClass() method of Ember.RenderBuffer.

    @type Array
    @default []
  */
  elementClasses: null,

  /**
    The id in of the element, to be applied in the id="" attribute

    You should not set this property yourself, rather, you should use
    the id() method of Ember.RenderBuffer.

    @type String
    @default null
  */
  elementId: null,

  /**
    A hash keyed on the name of the attribute and whose value will be
    applied to that attribute. For example, if you wanted to apply a
    data-view="Foo.bar" property to an element, you would set the
    elementAttributes hash to {'data-view':'Foo.bar'}

    You should not maintain this hash yourself, rather, you should use
    the attr() method of Ember.RenderBuffer.

    @type Hash
    @default {}
  */
  elementAttributes: null,

  /**
    The tagname of the element an instance of Ember.RenderBuffer represents.

    Usually, this gets set as the first parameter to Ember.RenderBuffer. For
    example, if you wanted to create a `p` tag, then you would call

      Ember.RenderBuffer('p')

    @type String
    @default null
  */
  elementTag: null,

  /**
    A hash keyed on the name of the style attribute and whose value will
    be applied to that attribute. For example, if you wanted to apply a
    background-color:black;" style to an element, you would set the
    elementStyle hash to {'background-color':'black'}

    You should not maintain this hash yourself, rather, you should use
    the style() method of Ember.RenderBuffer.

    @type Hash
    @default {}
  */
  elementStyle: null,

  /**
    Nested RenderBuffers will set this to their parent RenderBuffer
    instance.

    @type Ember._RenderBuffer
  */
  parentBuffer: null,

  /** @private */
  init: function() {
    this._super();

    set(this ,'elementClasses', Ember.A());
    set(this, 'elementAttributes', {});
    set(this, 'elementStyle', {});
    set(this, 'childBuffers', Ember.A());
    set(this, 'elements', {});
  },

  /**
    Adds a string of HTML to the RenderBuffer.

    @param {String} string HTML to push into the buffer
    @returns {Ember.RenderBuffer} this
  */
  push: function(string) {
    get(this, 'childBuffers').pushObject(String(string));
    return this;
  },

  /**
    Adds a class to the buffer, which will be rendered to the class attribute.

    @param {String} className Class name to add to the buffer
    @returns {Ember.RenderBuffer} this
  */
  addClass: function(className) {
    get(this, 'elementClasses').addObject(className);
    return this;
  },

  /**
    Sets the elementID to be used for the element.

    @param {String} id
    @returns {Ember.RenderBuffer} this
  */
  id: function(id) {
    set(this, 'elementId', id);
    return this;
  },

  /**
    Adds an attribute which will be rendered to the element.

    @param {String} name The name of the attribute
    @param {String} value The value to add to the attribute
    @returns {Ember.RenderBuffer} this
  */
  attr: function(name, value) {
    get(this, 'elementAttributes')[name] = value;
    return this;
  },

  /**
    Adds a style to the style attribute which will be rendered to the element.

    @param {String} name Name of the style
    @param {String} value
    @returns {Ember.RenderBuffer} this
  */
  style: function(name, value) {
    get(this, 'elementStyle')[name] = value;
    return this;
  },

  /**
    Create a new child render buffer from a parent buffer. Optionally set
    additional properties on the buffer. Optionally invoke a callback
    with the newly created buffer.

    This is a primitive method used by other public methods: `begin`,
    `prepend`, `replaceWith`, `insertAfter`.

    @private
    @param {String} tagName Tag name to use for the child buffer's element
    @param {Ember._RenderBuffer} parent The parent render buffer that this
      buffer should be appended to.
    @param {Function} fn A callback to invoke with the newly created buffer.
    @param {Object} other Additional properties to add to the newly created
      buffer.
  */
  newBuffer: function(tagName, parent, fn, other) {
    var buffer = Ember._RenderBuffer.create({
      parentBuffer: parent,
      elementTag: tagName
    });

    if (other) { buffer.setProperties(other); }
    if (fn) { fn.call(this, buffer); }

    return buffer;
  },

  /**
    Replace the current buffer with a new buffer. This is a primitive
    used by `remove`, which passes `null` for `newBuffer`, and `replaceWith`,
    which passes the new buffer it created.

    @private
    @param {Ember._RenderBuffer} buffer The buffer to insert in place of
      the existing buffer.
  */
  replaceWithBuffer: function(newBuffer) {
    var parent = get(this, 'parentBuffer');
    if (!parent) { return; }

    var childBuffers = get(parent, 'childBuffers');

    var index = childBuffers.indexOf(this);

    if (newBuffer) {
      childBuffers.splice(index, 1, newBuffer);
    } else {
      childBuffers.splice(index, 1);
    }
  },

  /**
    Creates a new Ember.RenderBuffer object with the provided tagName as
    the element tag and with its parentBuffer property set to the current
    Ember.RenderBuffer.

    @param {String} tagName Tag name to use for the child buffer's element
    @returns {Ember.RenderBuffer} A new RenderBuffer object
  */
  begin: function(tagName) {
    return this.newBuffer(tagName, this, function(buffer) {
      get(this, 'childBuffers').pushObject(buffer);
    });
  },

  /**
    Prepend a new child buffer to the current render buffer.

    @param {String} tagName Tag name to use for the child buffer's element
  */
  prepend: function(tagName) {
    return this.newBuffer(tagName, this, function(buffer) {
      get(this, 'childBuffers').insertAt(0, buffer);
    });
  },

  /**
    Replace the current buffer with a new render buffer.

    @param {String} tagName Tag name to use for the new buffer's element
  */
  replaceWith: function(tagName) {
    var parentBuffer = get(this, 'parentBuffer');

    return this.newBuffer(tagName, parentBuffer, function(buffer) {
      this.replaceWithBuffer(buffer);
    });
  },

  /**
    Insert a new render buffer after the current render buffer.

    @param {String} tagName Tag name to use for the new buffer's element
  */
  insertAfter: function(tagName) {
    var parentBuffer = get(this, 'parentBuffer');

    return this.newBuffer(tagName, parentBuffer, function(buffer) {
      var siblings = get(parentBuffer, 'childBuffers');
      var index = siblings.indexOf(this);
      siblings.insertAt(index + 1, buffer);
    });
  },

  /**
    Closes the current buffer and adds its content to the parentBuffer.

    @returns {Ember.RenderBuffer} The parentBuffer, if one exists. Otherwise, this
  */
  end: function() {
    var parent = get(this, 'parentBuffer');
    return parent || this;
  },

  remove: function() {
    this.replaceWithBuffer(null);
  },

  /**
    @returns {DOMElement} The element corresponding to the generated HTML
      of this buffer
  */
  element: function() {
    return Ember.$(this.string())[0];
  },

  /**
    Generates the HTML content for this buffer.

    @returns {String} The generated HTMl
  */
  string: function() {
    var id = get(this, 'elementId'),
        classes = get(this, 'elementClasses'),
        attrs = get(this, 'elementAttributes'),
        style = get(this, 'elementStyle'),
        tag = get(this, 'elementTag'),
        content = '',
        styleBuffer = [], prop;

    if (tag) {
      var openTag = ["<" + tag];

      if (id) { openTag.push('id="' + id + '"'); }
      if (classes.length) { openTag.push('class="' + classes.join(" ") + '"'); }

      if (!jQuery.isEmptyObject(style)) {
        for (prop in style) {
          if (style.hasOwnProperty(prop)) {
            styleBuffer.push(prop + ':' + style[prop] + ';');
          }
        }

        openTag.push('style="' + styleBuffer.join("") + '"');
      }

      for (prop in attrs) {
        if (attrs.hasOwnProperty(prop)) {
          openTag.push(prop + '="' + attrs[prop] + '"');
        }
      }

      openTag = openTag.join(" ") + '>';
    }

    var childBuffers = get(this, 'childBuffers');

    childBuffers.forEach(function(buffer) {
      var stringy = typeof buffer === 'string';
      content += (stringy ? buffer : buffer.string());
    });

    if (tag) {
      return openTag + content + "</" + tag + ">";
    } else {
      return content;
    }
  }

});
<commit_msg>childBuffers in RenderBuffer really don't need to be an Ember Array<commit_after>// ==========================================================================
// Project:   Ember - JavaScript Application Framework
// Copyright: ©2006-2011 Strobe Inc. and contributors.
//            Portions ©2008-2011 Apple Inc. All rights reserved.
// License:   Licensed under MIT license (see license.js)
// ==========================================================================

var get = Ember.get, set = Ember.set;

/**
  @class

  Ember.RenderBuffer gathers information regarding the a view and generates the
  final representation. Ember.RenderBuffer will generate HTML which can be pushed
  to the DOM.

  @extends Ember.Object
*/
Ember.RenderBuffer = function(tagName) {
  return Ember._RenderBuffer.create({ elementTag: tagName });
};

Ember._RenderBuffer = Ember.Object.extend(
/** @scope Ember.RenderBuffer.prototype */ {

  /**
    Array of class-names which will be applied in the class="" attribute

    You should not maintain this array yourself, rather, you should use
    the addClass() method of Ember.RenderBuffer.

    @type Array
    @default []
  */
  elementClasses: null,

  /**
    The id in of the element, to be applied in the id="" attribute

    You should not set this property yourself, rather, you should use
    the id() method of Ember.RenderBuffer.

    @type String
    @default null
  */
  elementId: null,

  /**
    A hash keyed on the name of the attribute and whose value will be
    applied to that attribute. For example, if you wanted to apply a
    data-view="Foo.bar" property to an element, you would set the
    elementAttributes hash to {'data-view':'Foo.bar'}

    You should not maintain this hash yourself, rather, you should use
    the attr() method of Ember.RenderBuffer.

    @type Hash
    @default {}
  */
  elementAttributes: null,

  /**
    The tagname of the element an instance of Ember.RenderBuffer represents.

    Usually, this gets set as the first parameter to Ember.RenderBuffer. For
    example, if you wanted to create a `p` tag, then you would call

      Ember.RenderBuffer('p')

    @type String
    @default null
  */
  elementTag: null,

  /**
    A hash keyed on the name of the style attribute and whose value will
    be applied to that attribute. For example, if you wanted to apply a
    background-color:black;" style to an element, you would set the
    elementStyle hash to {'background-color':'black'}

    You should not maintain this hash yourself, rather, you should use
    the style() method of Ember.RenderBuffer.

    @type Hash
    @default {}
  */
  elementStyle: null,

  /**
    Nested RenderBuffers will set this to their parent RenderBuffer
    instance.

    @type Ember._RenderBuffer
  */
  parentBuffer: null,

  /** @private */
  init: function() {
    this._super();

    set(this ,'elementClasses', Ember.A());
    set(this, 'elementAttributes', {});
    set(this, 'elementStyle', {});
    set(this, 'childBuffers', []);
    set(this, 'elements', {});
  },

  /**
    Adds a string of HTML to the RenderBuffer.

    @param {String} string HTML to push into the buffer
    @returns {Ember.RenderBuffer} this
  */
  push: function(string) {
    get(this, 'childBuffers').push(String(string));
    return this;
  },

  /**
    Adds a class to the buffer, which will be rendered to the class attribute.

    @param {String} className Class name to add to the buffer
    @returns {Ember.RenderBuffer} this
  */
  addClass: function(className) {
    get(this, 'elementClasses').addObject(className);
    return this;
  },

  /**
    Sets the elementID to be used for the element.

    @param {String} id
    @returns {Ember.RenderBuffer} this
  */
  id: function(id) {
    set(this, 'elementId', id);
    return this;
  },

  /**
    Adds an attribute which will be rendered to the element.

    @param {String} name The name of the attribute
    @param {String} value The value to add to the attribute
    @returns {Ember.RenderBuffer} this
  */
  attr: function(name, value) {
    get(this, 'elementAttributes')[name] = value;
    return this;
  },

  /**
    Adds a style to the style attribute which will be rendered to the element.

    @param {String} name Name of the style
    @param {String} value
    @returns {Ember.RenderBuffer} this
  */
  style: function(name, value) {
    get(this, 'elementStyle')[name] = value;
    return this;
  },

  /**
    Create a new child render buffer from a parent buffer. Optionally set
    additional properties on the buffer. Optionally invoke a callback
    with the newly created buffer.

    This is a primitive method used by other public methods: `begin`,
    `prepend`, `replaceWith`, `insertAfter`.

    @private
    @param {String} tagName Tag name to use for the child buffer's element
    @param {Ember._RenderBuffer} parent The parent render buffer that this
      buffer should be appended to.
    @param {Function} fn A callback to invoke with the newly created buffer.
    @param {Object} other Additional properties to add to the newly created
      buffer.
  */
  newBuffer: function(tagName, parent, fn, other) {
    var buffer = Ember._RenderBuffer.create({
      parentBuffer: parent,
      elementTag: tagName
    });

    if (other) { buffer.setProperties(other); }
    if (fn) { fn.call(this, buffer); }

    return buffer;
  },

  /**
    Replace the current buffer with a new buffer. This is a primitive
    used by `remove`, which passes `null` for `newBuffer`, and `replaceWith`,
    which passes the new buffer it created.

    @private
    @param {Ember._RenderBuffer} buffer The buffer to insert in place of
      the existing buffer.
  */
  replaceWithBuffer: function(newBuffer) {
    var parent = get(this, 'parentBuffer');
    if (!parent) { return; }

    var childBuffers = get(parent, 'childBuffers');

    var index = childBuffers.indexOf(this);

    if (newBuffer) {
      childBuffers.splice(index, 1, newBuffer);
    } else {
      childBuffers.splice(index, 1);
    }
  },

  /**
    Creates a new Ember.RenderBuffer object with the provided tagName as
    the element tag and with its parentBuffer property set to the current
    Ember.RenderBuffer.

    @param {String} tagName Tag name to use for the child buffer's element
    @returns {Ember.RenderBuffer} A new RenderBuffer object
  */
  begin: function(tagName) {
    return this.newBuffer(tagName, this, function(buffer) {
      get(this, 'childBuffers').push(buffer);
    });
  },

  /**
    Prepend a new child buffer to the current render buffer.

    @param {String} tagName Tag name to use for the child buffer's element
  */
  prepend: function(tagName) {
    return this.newBuffer(tagName, this, function(buffer) {
      get(this, 'childBuffers').splice(0, 0, buffer);
    });
  },

  /**
    Replace the current buffer with a new render buffer.

    @param {String} tagName Tag name to use for the new buffer's element
  */
  replaceWith: function(tagName) {
    var parentBuffer = get(this, 'parentBuffer');

    return this.newBuffer(tagName, parentBuffer, function(buffer) {
      this.replaceWithBuffer(buffer);
    });
  },

  /**
    Insert a new render buffer after the current render buffer.

    @param {String} tagName Tag name to use for the new buffer's element
  */
  insertAfter: function(tagName) {
    var parentBuffer = get(this, 'parentBuffer');

    return this.newBuffer(tagName, parentBuffer, function(buffer) {
      var siblings = get(parentBuffer, 'childBuffers');
      var index = siblings.indexOf(this);
      siblings.splice(index + 1, 0, buffer);
    });
  },

  /**
    Closes the current buffer and adds its content to the parentBuffer.

    @returns {Ember.RenderBuffer} The parentBuffer, if one exists. Otherwise, this
  */
  end: function() {
    var parent = get(this, 'parentBuffer');
    return parent || this;
  },

  remove: function() {
    this.replaceWithBuffer(null);
  },

  /**
    @returns {DOMElement} The element corresponding to the generated HTML
      of this buffer
  */
  element: function() {
    return Ember.$(this.string())[0];
  },

  /**
    Generates the HTML content for this buffer.

    @returns {String} The generated HTMl
  */
  string: function() {
    var id = get(this, 'elementId'),
        classes = get(this, 'elementClasses'),
        attrs = get(this, 'elementAttributes'),
        style = get(this, 'elementStyle'),
        tag = get(this, 'elementTag'),
        content = '',
        styleBuffer = [], prop;

    if (tag) {
      var openTag = ["<" + tag];

      if (id) { openTag.push('id="' + id + '"'); }
      if (classes.length) { openTag.push('class="' + classes.join(" ") + '"'); }

      if (!jQuery.isEmptyObject(style)) {
        for (prop in style) {
          if (style.hasOwnProperty(prop)) {
            styleBuffer.push(prop + ':' + style[prop] + ';');
          }
        }

        openTag.push('style="' + styleBuffer.join("") + '"');
      }

      for (prop in attrs) {
        if (attrs.hasOwnProperty(prop)) {
          openTag.push(prop + '="' + attrs[prop] + '"');
        }
      }

      openTag = openTag.join(" ") + '>';
    }

    var childBuffers = get(this, 'childBuffers');

    childBuffers.forEach(function(buffer) {
      var stringy = typeof buffer === 'string';
      content += (stringy ? buffer : buffer.string());
    });

    if (tag) {
      return openTag + content + "</" + tag + ">";
    } else {
      return content;
    }
  }

});
<|endoftext|>