<commit_before>import { dispatch as d3_dispatch } from 'd3-dispatch';

import {
    event as d3_event,
    select as d3_select,
    selectAll as d3_selectAll
} from 'd3-selection';

import {
    modeAddArea,
    modeAddLine,
    modeAddPoint
} from '../modes';

import { t, textDirection } from '../util/locale';
import { actionChangePreset } from '../actions/index';
import { operationDelete } from '../operations/index';
import { svgIcon } from '../svg/index';
import { tooltip } from '../util/tooltip';
import { uiPresetFavorite } from './preset_favorite';
import { uiPresetIcon } from './preset_icon';
import { uiTagReference } from './tag_reference';
import { utilKeybinding, utilNoAuto, utilRebind } from '../util';

export function uiSearchAdd(context) {
    var dispatch = d3_dispatch('choose');
    var presets;
    var search = d3_select(null), popover = d3_select(null), list = d3_select(null);

    function searchAdd(selection) {

        presets = context.presets().matchAnyGeometry(['point', 'line', 'area']);

        var searchWrap = selection
            .append('div')
            .attr('class', 'search-wrap');

        search = searchWrap
            .append('input')
            .attr('class', 'search-input')
            .attr('placeholder', t('modes.add_feature.title'))
            .attr('type', 'search')
            .call(utilNoAuto)
            .on('keypress', function() {
                // enter/return
                if (d3_event.keyCode === 13) {
                    popover.selectAll('.list .list-item.focused button.choose')
                        .each(function(d) { d.choose.call(this); });
                    d3_event.preventDefault();
                    d3_event.stopPropagation();
                }
            })
            .on('keydown', function(){
                // up/down arrow key navigation

                var nextFocus,
                    priorFocus;
                if (d3_event.keyCode === utilKeybinding.keyCodes['↓']) {
                    d3_event.preventDefault();
                    d3_event.stopPropagation();

                    priorFocus = popover.selectAll('.list .list-item.focused');
                    if (priorFocus.empty()) {
                        nextFocus = popover.selectAll('.list > .list-item:first-child');
                    } else {
                        nextFocus = popover.selectAll('.list .list-item.focused + .list-item');
                        if (nextFocus.empty()) {
                            nextFocus = d3_select(priorFocus.nodes()[0].nextElementSibling)
                                .selectAll('.list-item:first-child');
                        }
                    }
                    if (!nextFocus.empty()) {
                        focusListItem(nextFocus);
                        priorFocus.classed('focused', false);
                    }

                } else if (d3_event.keyCode === utilKeybinding.keyCodes['↑']) {
                    d3_event.preventDefault();
                    d3_event.stopPropagation();

                    priorFocus = popover.selectAll('.list .list-item.focused');
                    if (!priorFocus.empty()) {
                        nextFocus = d3_select(priorFocus.nodes()[0].previousElementSibling);
                        if (!nextFocus.empty() && !nextFocus.classed('list-item')) {
                            nextFocus = nextFocus.selectAll('.list-item:last-child');
                        }
                        if (!nextFocus.empty()) {
                            focusListItem(nextFocus);
                            priorFocus.classed('focused', false);
                        }
                    }
                }
            })
            .on('mousedown', function() {
                search.attr('clicking', true);
            })
            .on('mouseup', function() {
                search.attr('clicking', null);
            })
            .on('focus', function() {
                if (search.attr('clicking')) {
                    search.attr('focusing', true);
                    search.attr('clicking', null);
                } else {
                    search.node().setSelectionRange(0, search.property('value').length);
                }
                popover.classed('hide', false);
            })
            .on('blur', function() {
                popover.classed('hide', true);
            })
            .on('click', function() {
                if (search.attr('focusing')) {
                    search.node().setSelectionRange(0, search.property('value').length);
                    search.attr('focusing', null);
                }
            })
            .on('input', function () {
                var value = search.property('value');
                //list.classed('filtered', value.length);
                if (value.length) {
                    popover.selectAll('.subsection').remove();
                    var results = presets.search(value);
                    list.call(drawList, results);
                    popover.selectAll('.list .list-item.focused')
                        .classed('focused', false);
                    focusListItem(popover.selectAll('.list > .list-item:first-child'));
                } else {
                    //list.call(drawList, context.presets().defaults(geometry, 36));
                }
            });

        searchWrap
            .call(svgIcon('#iD-icon-search', 'search-icon pre-text'));

        popover = selection
            .append('div')
            .attr('class', 'popover fillL hide')
            .on('mousedown', function() {
                // don't blur the search input (and thus close results)
                d3_event.preventDefault();
                d3_event.stopPropagation();
            });

        list = popover
            .append('div')
            .attr('class', 'list');//
            //.call(drawList, context.presets().defaults(geometry, 36));

        context.features().on('change.search-add', updateForFeatureHiddenState);

        context.keybinding().on('1', function() {
            search.node().focus();
            d3_event.preventDefault();
            d3_event.stopPropagation();
        });
    }

    function focusListItem(selection) {
        if (!selection.empty()) {
            selection.classed('focused', true);
            var node = selection.nodes()[0];
            var popoverNode = popover.node();
            var nodeRect = node.getBoundingClientRect();

            // scroll to keep the focused item visible
            if (node.offsetTop < popoverNode.scrollTop) {
                popoverNode.scrollTop = node.offsetTop;

            } else if (node.offsetTop + node.offsetHeight > popoverNode.scrollTop + popoverNode.offsetHeight) {
                popoverNode.scrollTop = node.offsetTop + node.offsetHeight - popoverNode.offsetHeight;
            }
        }
    }

    function drawList(list, presets) {

        var collection = presets.collection.map(function(preset) {
            if (preset.members) {
                return CategoryItem(preset);
            } else if (preset.visible()) {
                var supportedGeometry = preset.geometry.filter(function(geometry) {
                    return ['point', 'line', 'area'].indexOf(geometry) !== -1;
                }).sort();
                if (supportedGeometry.length === 1) {
                    return AddablePresetItem(preset, supportedGeometry[0]);
                }
                return MultiGeometryPresetItem(preset, supportedGeometry);
            }
        });

        var items = list.selectAll('.list-item')
            .data(collection, function(d) { return d.preset.id; });

        items.order();

        items.exit()
            .remove();

        items.enter();
        drawItems(items.enter());

        updateForFeatureHiddenState();
    }

    function drawItems(selection) {

        var row = selection
            .append('div')
            .attr('class', 'list-item')
            .attr('id', function(d) {
                var id = 'search-add-list-item-preset-' + d.preset.id.replace(/[^a-zA-Z\d:]/g, '-');
                if (d.geometry) {
                    id += '-' + d.geometry;
                }
                return id;
            })
            .on('mouseover', function(d) {
                list.selectAll('.list-item.focused')
                    .classed('focused', false);
                d3_select(this)
                    .classed('focused', true);
            })
            .on('mouseout', function(d) {
                d3_select(this)
                    .classed('focused', false);
            });

        row.append('button')
            .attr('class', 'choose')
            .on('click', function(d) {
                d.choose.call(this);
            });

        row.each(function(d) {
            d3_select(this).call(
                uiPresetIcon()
                    .geometry(d.geometry)
                    .preset(d.preset)
                    .sizeClass('small')
            );
        });
        var label = row.append('div')
            .attr('class', 'label');

        label.each(function(d) {
            if (!d.geometry) {
                d3_select(this)
                    .call(svgIcon((textDirection === 'rtl' ? '#iD-icon-backward' : '#iD-icon-forward'), 'inline'));
            }
        });

        label.append('span')
            .text(function(d) {
                if (d.isSubitem) {
                    return t('modes.add_' + d.geometry + '.title');
                }
                return d.preset.name();
            });

        row.each(function(d) {
            if (d.geometry) {
                var presetFavorite = uiPresetFavorite(d.preset,d.geometry, context, 'accessory');
                d3_select(this).call(presetFavorite.button);
            }
        });
    }

    function updateForFeatureHiddenState() {

        var listItem = d3_selectAll('.search-add .popover .list-item');

        // remove existing tooltips
        listItem.selectAll('button.choose').call(tooltip().destroyAny);

        listItem.each(function(item, index) {
            if (!item.geometry) return;

            var hiddenPresetFeaturesId = context.features().isHiddenPreset(item.preset, item.geometry);
            var isHiddenPreset = !!hiddenPresetFeaturesId;

            var button = d3_select(this).selectAll('button.choose');

            d3_select(this).classed('disabled', isHiddenPreset);
            button.classed('disabled', isHiddenPreset);

            if (isHiddenPreset) {
                var isAutoHidden = context.features().autoHidden(hiddenPresetFeaturesId);
                var tooltipIdSuffix = isAutoHidden ? 'zoom' : 'manual';
                var tooltipObj = { features: t('feature.' + hiddenPresetFeaturesId + '.description') };
                button.call(tooltip('dark')
                    .html(true)
                    .title(t('inspector.hidden_preset.' + tooltipIdSuffix, tooltipObj))
                    .placement(index < 2 ? 'bottom' : 'top')
                );
            }
        });
    }

    function CategoryItem(preset) {
        var item = {};
        item.preset = preset;
        item.choose = function() {
        };
        return item;
    }

    function MultiGeometryPresetItem(preset, geometries) {

        var subsection = d3_select(null);

        var item = {};
        item.preset = preset;
        item.geometries = geometries;
        item.choose = function() {
            var selection = d3_select(this);
            if (selection.classed('disabled')) return;

            var listItemNode = selection.node().closest('.list-item');

            var shouldExpand = !selection.classed('expanded');

            selection.classed('expanded', shouldExpand);

            var iconName = shouldExpand ?
                '#iD-icon-down' : (textDirection === 'rtl' ? '#iD-icon-backward' : '#iD-icon-forward');
            d3_select(listItemNode).selectAll('.label svg.icon use')
                .attr('href', iconName);

            if (shouldExpand) {
                var subitems = geometries.map(function(geometry) {
                    return AddablePresetItem(preset, geometry, true);
                });
                var selector = '#' + listItemNode.id + ' + *';
                subsection = d3_selectAll('.search-add .popover .list').insert('div', selector)
                    .attr('class', 'subsection');
                var subitemsEnter = subsection.selectAll('.list-item')
                    .data(subitems)
                    .enter();
                drawItems(subitemsEnter);
                updateForFeatureHiddenState();
            } else {
                subsection.remove();
            }
        };
        return item;
    }

    function AddablePresetItem(preset, geometry, isSubitem) {
        var item = {};
        item.isSubitem = isSubitem;
        item.preset = preset;
        item.geometry = geometry;
        item.choose = function() {
            if (d3_select(this).classed('disabled')) return;

            var markerClass = 'add-preset add-' + geometry +
                ' add-preset-' + preset.name().replace(/\s+/g, '_') + '-' + geometry;
            var modeInfo = {
                id: markerClass,
                button: markerClass,
                preset: preset,
                geometry: geometry
            };
            var mode;
            switch (geometry) {
                case 'point':
                case 'vertex':
                    mode = modeAddPoint(context, modeInfo);
                    break;
                case 'line':
                    mode = modeAddLine(context, modeInfo);
                    break;
                case 'area':
                    mode = modeAddArea(context, modeInfo);
            }
            search.node().blur();
            context.enter(mode);
        };
        return item;
    }

    return utilRebind(searchAdd, dispatch, 'on');
}
<commit_msg>Make category items expandable<commit_after>import { dispatch as d3_dispatch } from 'd3-dispatch';

import {
    event as d3_event,
    select as d3_select,
    selectAll as d3_selectAll
} from 'd3-selection';

import {
    modeAddArea,
    modeAddLine,
    modeAddPoint
} from '../modes';

import { t, textDirection } from '../util/locale';
import { actionChangePreset } from '../actions/index';
import { operationDelete } from '../operations/index';
import { svgIcon } from '../svg/index';
import { tooltip } from '../util/tooltip';
import { uiPresetFavorite } from './preset_favorite';
import { uiPresetIcon } from './preset_icon';
import { uiTagReference } from './tag_reference';
import { utilKeybinding, utilNoAuto, utilRebind } from '../util';

export function uiSearchAdd(context) {
    var dispatch = d3_dispatch('choose');
    var presets;
    var search = d3_select(null), popover = d3_select(null), list = d3_select(null);

    function searchAdd(selection) {

        presets = context.presets().matchAnyGeometry(['point', 'line', 'area']);

        var searchWrap = selection
            .append('div')
            .attr('class', 'search-wrap');

        search = searchWrap
            .append('input')
            .attr('class', 'search-input')
            .attr('placeholder', t('modes.add_feature.title'))
            .attr('type', 'search')
            .call(utilNoAuto)
            .on('keypress', function() {
                // enter/return
                if (d3_event.keyCode === 13) {
                    popover.selectAll('.list .list-item.focused button.choose')
                        .each(function(d) { d.choose.call(this); });
                    d3_event.preventDefault();
                    d3_event.stopPropagation();
                }
            })
            .on('keydown', function(){
                // up/down arrow key navigation

                var nextFocus,
                    priorFocus;
                if (d3_event.keyCode === utilKeybinding.keyCodes['↓']) {
                    d3_event.preventDefault();
                    d3_event.stopPropagation();

                    priorFocus = popover.selectAll('.list .list-item.focused');
                    if (priorFocus.empty()) {
                        nextFocus = popover.selectAll('.list > .list-item:first-child');
                    } else {
                        nextFocus = popover.selectAll('.list .list-item.focused + .list-item');
                        if (nextFocus.empty()) {
                            nextFocus = d3_select(priorFocus.nodes()[0].nextElementSibling)
                                .selectAll('.list-item:first-child');
                        }
                    }
                    if (!nextFocus.empty()) {
                        focusListItem(nextFocus);
                        priorFocus.classed('focused', false);
                    }

                } else if (d3_event.keyCode === utilKeybinding.keyCodes['↑']) {
                    d3_event.preventDefault();
                    d3_event.stopPropagation();

                    priorFocus = popover.selectAll('.list .list-item.focused');
                    if (!priorFocus.empty()) {
                        nextFocus = d3_select(priorFocus.nodes()[0].previousElementSibling);
                        if (!nextFocus.empty() && !nextFocus.classed('list-item')) {
                            nextFocus = nextFocus.selectAll('.list-item:last-child');
                        }
                        if (!nextFocus.empty()) {
                            focusListItem(nextFocus);
                            priorFocus.classed('focused', false);
                        }
                    }
                }
            })
            .on('mousedown', function() {
                search.attr('clicking', true);
            })
            .on('mouseup', function() {
                search.attr('clicking', null);
            })
            .on('focus', function() {
                if (search.attr('clicking')) {
                    search.attr('focusing', true);
                    search.attr('clicking', null);
                } else {
                    search.node().setSelectionRange(0, search.property('value').length);
                }
                popover.classed('hide', false);
            })
            .on('blur', function() {
                popover.classed('hide', true);
            })
            .on('click', function() {
                if (search.attr('focusing')) {
                    search.node().setSelectionRange(0, search.property('value').length);
                    search.attr('focusing', null);
                }
            })
            .on('input', function () {
                var value = search.property('value');
                //list.classed('filtered', value.length);
                if (value.length) {
                    popover.selectAll('.subsection').remove();
                    var results = presets.search(value);
                    list.call(drawList, results);
                    popover.selectAll('.list .list-item.focused')
                        .classed('focused', false);
                    focusListItem(popover.selectAll('.list > .list-item:first-child'));
                } else {
                    //list.call(drawList, context.presets().defaults(geometry, 36));
                }
            });

        searchWrap
            .call(svgIcon('#iD-icon-search', 'search-icon pre-text'));

        popover = selection
            .append('div')
            .attr('class', 'popover fillL hide')
            .on('mousedown', function() {
                // don't blur the search input (and thus close results)
                d3_event.preventDefault();
                d3_event.stopPropagation();
            });

        list = popover
            .append('div')
            .attr('class', 'list');//
            //.call(drawList, context.presets().defaults(geometry, 36));

        context.features().on('change.search-add', updateForFeatureHiddenState);

        context.keybinding().on('1', function() {
            search.node().focus();
            d3_event.preventDefault();
            d3_event.stopPropagation();
        });
    }

    function focusListItem(selection) {
        if (!selection.empty()) {
            selection.classed('focused', true);
            var node = selection.nodes()[0];
            var popoverNode = popover.node();
            var nodeRect = node.getBoundingClientRect();

            // scroll to keep the focused item visible
            if (node.offsetTop < popoverNode.scrollTop) {
                popoverNode.scrollTop = node.offsetTop;

            } else if (node.offsetTop + node.offsetHeight > popoverNode.scrollTop + popoverNode.offsetHeight) {
                popoverNode.scrollTop = node.offsetTop + node.offsetHeight - popoverNode.offsetHeight;
            }
        }
    }

    function drawList(list, presets) {

        var collection = presets.collection.map(function(preset) {
            if (preset.members) {
                return CategoryItem(preset);
            } else if (preset.visible()) {
                var supportedGeometry = preset.geometry.filter(function(geometry) {
                    return ['point', 'line', 'area'].indexOf(geometry) !== -1;
                }).sort();
                if (supportedGeometry.length === 1) {
                    return AddablePresetItem(preset, supportedGeometry[0]);
                }
                return MultiGeometryPresetItem(preset, supportedGeometry);
            }
        });

        var items = list.selectAll('.list-item')
            .data(collection, function(d) { return d.preset.id; });

        items.order();

        items.exit()
            .remove();

        items.enter();
        drawItems(items.enter());

        updateForFeatureHiddenState();
    }

    function drawItems(selection) {

        var row = selection
            .append('div')
            .attr('class', 'list-item')
            .attr('id', function(d) {
                var id = 'search-add-list-item-preset-' + d.preset.id.replace(/[^a-zA-Z\d:]/g, '-');
                if (d.geometry) {
                    id += '-' + d.geometry;
                }
                return id;
            })
            .on('mouseover', function(d) {
                list.selectAll('.list-item.focused')
                    .classed('focused', false);
                d3_select(this)
                    .classed('focused', true);
            })
            .on('mouseout', function(d) {
                d3_select(this)
                    .classed('focused', false);
            });

        row.append('button')
            .attr('class', 'choose')
            .on('click', function(d) {
                d.choose.call(this);
            });

        row.each(function(d) {
            d3_select(this).call(
                uiPresetIcon()
                    .geometry(d.geometry)
                    .preset(d.preset)
                    .sizeClass('small')
            );
        });
        var label = row.append('div')
            .attr('class', 'label');

        label.each(function(d) {
            if (!d.geometry) {
                d3_select(this)
                    .call(svgIcon((textDirection === 'rtl' ? '#iD-icon-backward' : '#iD-icon-forward'), 'inline'));
            }
        });

        label.append('span')
            .text(function(d) {
                if (d.isSubitem) {
                    return t('modes.add_' + d.geometry + '.title');
                }
                return d.preset.name();
            });

        row.each(function(d) {
            if (d.geometry) {
                var presetFavorite = uiPresetFavorite(d.preset,d.geometry, context, 'accessory');
                d3_select(this).call(presetFavorite.button);
            }
        });
    }

    function updateForFeatureHiddenState() {

        var listItem = d3_selectAll('.search-add .popover .list-item');

        // remove existing tooltips
        listItem.selectAll('button.choose').call(tooltip().destroyAny);

        listItem.each(function(item, index) {
            if (!item.geometry) return;

            var hiddenPresetFeaturesId = context.features().isHiddenPreset(item.preset, item.geometry);
            var isHiddenPreset = !!hiddenPresetFeaturesId;

            var button = d3_select(this).selectAll('button.choose');

            d3_select(this).classed('disabled', isHiddenPreset);
            button.classed('disabled', isHiddenPreset);

            if (isHiddenPreset) {
                var isAutoHidden = context.features().autoHidden(hiddenPresetFeaturesId);
                var tooltipIdSuffix = isAutoHidden ? 'zoom' : 'manual';
                var tooltipObj = { features: t('feature.' + hiddenPresetFeaturesId + '.description') };
                button.call(tooltip('dark')
                    .html(true)
                    .title(t('inspector.hidden_preset.' + tooltipIdSuffix, tooltipObj))
                    .placement(index < 2 ? 'bottom' : 'top')
                );
            }
        });
    }

    function chooseExpandable(item, itemSelection) {

        var shouldExpand = !itemSelection.classed('expanded');

        itemSelection.classed('expanded', shouldExpand);

        var iconName = shouldExpand ?
            '#iD-icon-down' : (textDirection === 'rtl' ? '#iD-icon-backward' : '#iD-icon-forward');
        itemSelection.selectAll('.label svg.icon use')
            .attr('href', iconName);

        if (shouldExpand) {
            var subitems = item.subitems();
            var selector = '#' + itemSelection.node().id + ' + *';
            item.subsection = d3_selectAll('.search-add .popover .list').insert('div', selector)
                .attr('class', 'subsection');
            var subitemsEnter = item.subsection.selectAll('.list-item')
                .data(subitems)
                .enter();
            drawItems(subitemsEnter);
            updateForFeatureHiddenState();
        } else {
            item.subsection.remove();
        }
    }

    function CategoryItem(preset) {
        var item = {};
        item.subsection = d3_select(null);
        item.preset = preset;
        item.choose = function() {
            var selection = d3_select(this);
            if (selection.classed('disabled')) return;
            chooseExpandable(item, d3_select(selection.node().closest('.list-item')));
        };
        item.subitems = function() {
            return preset.members.collection.map(function(preset) {
                var supportedGeometry = preset.geometry.filter(function(geometry) {
                    return ['point', 'line', 'area'].indexOf(geometry) !== -1;
                }).sort();
                if (supportedGeometry.length === 1) {
                    return AddablePresetItem(preset, supportedGeometry[0]);
                }
                return MultiGeometryPresetItem(preset, supportedGeometry);
            });
        }
        return item;
    }

    function MultiGeometryPresetItem(preset, geometries) {

        var item = {};
        item.subsection = d3_select(null);
        item.preset = preset;
        item.geometries = geometries;
        item.choose = function() {
            var selection = d3_select(this);
            if (selection.classed('disabled')) return;
            chooseExpandable(item, d3_select(selection.node().closest('.list-item')));
        };
        item.subitems = function() {
            return geometries.map(function(geometry) {
                return AddablePresetItem(preset, geometry, true);
            });
        };
        return item;
    }

    function AddablePresetItem(preset, geometry, isSubitem) {
        var item = {};
        item.isSubitem = isSubitem;
        item.preset = preset;
        item.geometry = geometry;
        item.choose = function() {
            if (d3_select(this).classed('disabled')) return;

            var markerClass = 'add-preset add-' + geometry +
                ' add-preset-' + preset.name().replace(/\s+/g, '_') + '-' + geometry;
            var modeInfo = {
                id: markerClass,
                button: markerClass,
                preset: preset,
                geometry: geometry
            };
            var mode;
            switch (geometry) {
                case 'point':
                case 'vertex':
                    mode = modeAddPoint(context, modeInfo);
                    break;
                case 'line':
                    mode = modeAddLine(context, modeInfo);
                    break;
                case 'area':
                    mode = modeAddArea(context, modeInfo);
            }
            search.node().blur();
            context.enter(mode);
        };
        return item;
    }

    return utilRebind(searchAdd, dispatch, 'on');
}
<|endoftext|>


<commit_before>;(function(window, undefined) {
	"use strict";
	var ess,emptyArray = [],slice = emptyArray.slice,filter = emptyArray.filter,elementTypes = [1, 9, 11],
		WCJ = (function(){
		var WCJ = function( selector ) {
		    return new WCJ.fn.init(selector);
		};
		WCJ.fn = WCJ.prototype = {
			init:function( selector ){
				var dom ;
    			if (!selector)
    				dom = emptyArray,dom.selector = selector || '',dom.__proto__ = WCJ.fn.init.prototype;
    			else {
    				if (WCJ.isArray(selector))
    					dom = selector;
	                else if (elementTypes.indexOf(selector.nodeType) >= 0 || selector === window)
	                	dom = [selector], selector = null;
	                else dom = (function(){
				        var found;
				        return (document && /^#([\w-]+)$/.test(selector))?
				        ((found = document.getElementById(RegExp.$1)) ? [found] : emptyArray ):
					    slice.call(
						    /^\.([\w-]+)$/.test(selector) ? document.getElementsByClassName(RegExp.$1) :
						    /^[\w-]+$/.test(selector) ? document.getElementsByTagName(selector) :
						    document.querySelectorAll(selector)
					    );
					})();
					dom = dom || emptyArray;
					dom.__proto__ = WCJ.fn.init.prototype;
					dom.selector = selector || '';
    			}
    			return dom;
			},
			size:function(){return this.length;}
		}
		WCJ.fn.init.prototype = WCJ.fn;
        WCJ.extend = WCJ.fn.extend = function () {
            var options, name, src, copy,
			target = arguments[0] || {},
			i = 1,
			length = arguments.length,
			deep = false;
            //处理深拷贝的情况
            if (typeof (target) === "boolean") {
                deep = target;
                target = arguments[1] || {};
                i = 2;
            }
            //处理时，目标是一个字符串或（深拷贝可能的情况下）的东西
            if (typeof (target) !== "object" && !WCJ.isFunction(target)) {
                target = {};
            }
            //扩展WCJ的本身，如果只有一个参数传递
            if (length === i) {
                target = this;
                --i;
            }
            for (; i < length; i++) {
                if ((options = arguments[i]) != null) {
                    for (name in options) {
                        src = target[name];
                        copy = options[name];
                        if (target === copy) {
                            continue;
                        }
                        if (copy !== undefined) {
                            target[name] = copy;
                        }
                    }
                }
            }
            return target;
        };
		return WCJ;
	})();

	WCJ.fn.extend({
		forEach: emptyArray.forEach,
		concat: emptyArray.concat,
	    each: function(callback){
	      this.forEach(function(el, idx){ callback.call(el, idx, el) });
	      return this;
	    },
	    map: function(fn){
	    	return WCJ(WCJ.map(this, function(el, i){ return fn.call(el, i, el) }));
	    },
	    get: function(index){
	      return index === undefined ? slice.call(this) : this[index >= 0 ? index : index + this.length];
	    },
	    empty: function(){ return this.each(function(){ this.innerHTML = '' }) },
	    text: function(text){
	      return text === undefined ?
	        (this.length > 0 ? this[0].textContent : null) :
	        this.each(function(){ this.textContent = text });
	    },
        bind: function(type, func) {
			this.each(function(){
				if (this.addEventListener) this.addEventListener(type, func, false);
				else if (this.attachEvent) this.attachEvent("on" + type, func);
				else this["on" + type] = func;
			})
			return this;
		},
		unbind:function(type, func) {
			this.each(function(ele,obj){
				if (this.removeEventListener) this.removeEventListener(type, func, false);
				else if (this[i].detachEvent) this.detachEvent("on" + type, func);
				else this["on" + type] = null;
			})
			return this;
		},
		filter:function(selector){
			return WCJ(filter.call(this, function(element){
        		return WCJ.matches(element, selector)
		    }))
		},
		pluck: function(property){ return WCJ.map(this, function(el){ return el[property] })},
		parent: function(){
			var arr=this.pluck('parentNode');
			return filter.call(arr, function(item, idx){
				return arr.indexOf(item) == idx
			});
		}
	});

	WCJ.extend({
		isDocument:function (obj) { return obj != null && obj.nodeType ? obj.nodeType == obj.DOCUMENT_NODE : false ;},
		isFunction:function (value) { return ({}).toString.call(value) == "[object Function]" },
		isObject:function (value) { return value instanceof Object },
		isArray:function (value) { return value instanceof Array },
		likeArray:function (obj) { return typeof obj.length == 'number' },
        type: function (obj) {
            switch (obj) {
                case null: return "Null";
                case (void 0): return "Undefined";
            }
            var type = typeof (obj);
            switch (type) {
                case 'boolean': return "Boolean";
                case 'number': return "Number";
                case 'string': return "String";
                case 'function': return "Function";
            }
            return "Object";
        },
        error:function(msg) {throw msg;},
        each:function(elements, callback){
			var i, key
			if (this.likeArray(elements)) {
				for (i = 0; i < elements.length; i++)
					if (callback.call(elements[i], i, elements[i]) === false) return elements
				} else {
				for (key in elements)
					if (callback.call(elements[key], key, elements[key]) === false) return elements
			}
			return elements
		},
		map:function(elements, callback){
			var value, values = [], i, key
			if (this.likeArray(elements))
			for (i = 0; i < elements.length; i++) {
				value = callback(elements[i], i)
				if (value != null) values.push(value)
			}
			else
			for (key in elements) {
				value = callback(elements[key], key)
				if (value != null) values.push(value)
			}
			return values.length > 0 ? WCJ.fn.concat.apply([], values) : values;
		},
		matches:function(element, selector){
			if (!selector || !element || element.nodeType !== 1) return false;
		    var matchesSelector = element.webkitMatchesSelector || element.mozMatchesSelector ||
		                          element.oMatchesSelector || element.msMatchesSelector || element.matchesSelector;
		    if (matchesSelector) return matchesSelector.call(element, selector);
		}
	});

	//解决低版本浏览器对filter方法的支持
	if (!Array.prototype.filter){
		Array.prototype.filter = function(fun /*, thisArg */){
			"use strict";
			if (this === void 0 || this === null)
				throw new TypeError();
			var t = Object(this);
			var len = t.length >>> 0;
			if (typeof fun !== "function")
				throw new TypeError();
			var res = [];
			var thisArg = arguments.length >= 2 ? arguments[1] : void 0;
			for (var i = 0; i < len; i++){
				if (i in t){
					var val = t[i];
					if (fun.call(thisArg, val, i, t))
					res.push(val);
				}
			}

			return res;
		};
	}

	//字符串处理
    WCJ.extend(String.prototype,{
        trim: function () {return this.replace(/(^\s*)|(\s*$)/g, "");},
        leftTrim: function () {return this.replace(/(^\s*)/g, "");}
    })
	window.WCJ = window.$$ = WCJ;
})(window);<commit_msg>parents获取对象集合每个元素所有的祖先元素（不包含根元素）<commit_after>;(function(window, undefined) {
	"use strict";
	var ess,emptyArray = [],slice = emptyArray.slice,filter = emptyArray.filter,elementTypes = [1, 9, 11],
		WCJ = (function(){
		var WCJ = function( selector ) {
		    return new WCJ.fn.init(selector);
		};
		WCJ.fn = WCJ.prototype = {
			init:function( selector ){
				var dom ;
    			if (!selector)
    				dom = emptyArray,dom.selector = selector || '',dom.__proto__ = WCJ.fn.init.prototype;
    			else {
    				if (WCJ.isArray(selector))
    					dom = selector;
	                else if (elementTypes.indexOf(selector.nodeType) >= 0 || selector === window)
	                	dom = [selector], selector = null;
	                else dom = (function(){
				        var found;
				        return (document && /^#([\w-]+)$/.test(selector))?
				        ((found = document.getElementById(RegExp.$1)) ? [found] : emptyArray ):
					    slice.call(
						    /^\.([\w-]+)$/.test(selector) ? document.getElementsByClassName(RegExp.$1) :
						    /^[\w-]+$/.test(selector) ? document.getElementsByTagName(selector) :
						    document.querySelectorAll(selector)
					    );
					})();
					dom = dom || emptyArray;
					dom.__proto__ = WCJ.fn.init.prototype;
					dom.selector = selector || '';
    			}
    			return dom;
			},
			size:function(){return this.length;}
		}
		WCJ.fn.init.prototype = WCJ.fn;
        WCJ.extend = WCJ.fn.extend = function () {
            var options, name, src, copy,
			target = arguments[0] || {},
			i = 1,
			length = arguments.length,
			deep = false;
            //处理深拷贝的情况
            if (typeof (target) === "boolean") {
                deep = target;
                target = arguments[1] || {};
                i = 2;
            }
            //处理时，目标是一个字符串或（深拷贝可能的情况下）的东西
            if (typeof (target) !== "object" && !WCJ.isFunction(target)) {
                target = {};
            }
            //扩展WCJ的本身，如果只有一个参数传递
            if (length === i) {
                target = this;
                --i;
            }
            for (; i < length; i++) {
                if ((options = arguments[i]) != null) {
                    for (name in options) {
                        src = target[name];
                        copy = options[name];
                        if (target === copy) {
                            continue;
                        }
                        if (copy !== undefined) {
                            target[name] = copy;
                        }
                    }
                }
            }
            return target;
        };
		return WCJ;
	})();

	WCJ.fn.extend({
		forEach: emptyArray.forEach,
		concat: emptyArray.concat,
	    each: function(callback){
	      this.forEach(function(el, idx){ callback.call(el, idx, el) });
	      return this;
	    },
	    map: function(fn){
	    	return WCJ(WCJ.map(this, function(el, i){ return fn.call(el, i, el) }));
	    },
	    get: function(index){
	      return index === undefined ? slice.call(this) : this[index >= 0 ? index : index + this.length];
	    },
	    empty: function(){ return this.each(function(){ this.innerHTML = '' }) },
	    text: function(text){
	      return text === undefined ?
	        (this.length > 0 ? this[0].textContent : null) :
	        this.each(function(){ this.textContent = text });
	    },
        bind: function(type, func) {
			this.each(function(){
				if (this.addEventListener) this.addEventListener(type, func, false);
				else if (this.attachEvent) this.attachEvent("on" + type, func);
				else this["on" + type] = func;
			})
			return this;
		},
		unbind:function(type, func) {
			this.each(function(ele,obj){
				if (this.removeEventListener) this.removeEventListener(type, func, false);
				else if (this[i].detachEvent) this.detachEvent("on" + type, func);
				else this["on" + type] = null;
			})
			return this;
		},
		filter:function(selector){
			return WCJ(filter.call(this, function(element){
        		return WCJ.matches(element, selector)
		    }))
		},
		pluck: function(property){ return WCJ.map(this, function(el){ return el[property] })},
		parent: function(){
			var arr=this.pluck('parentNode');
			return filter.call(arr, function(item, idx){
				return arr.indexOf(item) == idx
			});
		},
		parents: function(selector){
			var ancestors = [], nodes = this
			while (nodes.length > 0)
			nodes = WCJ.map(nodes, function(node){
			  if ((node = node.parentNode) && !WCJ.isDocument(node) && ancestors.indexOf(node) < 0) {
			    ancestors.push(node)
			    return node
			  }
			});
    		return selector == null ? WCJ(ancestors) : WCJ(ancestors).filter(selector);
		}
	});

	WCJ.extend({
		isDocument:function (obj) { return obj != null && obj.nodeType ? obj.nodeType == obj.DOCUMENT_NODE : false ;},
		isFunction:function (value) { return ({}).toString.call(value) == "[object Function]" },
		isObject:function (value) { return value instanceof Object },
		isArray:function (value) { return value instanceof Array },
		likeArray:function (obj) { return typeof obj.length == 'number' },
        type: function (obj) {
            switch (obj) {
                case null: return "Null";
                case (void 0): return "Undefined";
            }
            var type = typeof (obj);
            switch (type) {
                case 'boolean': return "Boolean";
                case 'number': return "Number";
                case 'string': return "String";
                case 'function': return "Function";
            }
            return "Object";
        },
        error:function(msg) {throw msg;},
        each:function(elements, callback){
			var i, key
			if (this.likeArray(elements)) {
				for (i = 0; i < elements.length; i++)
					if (callback.call(elements[i], i, elements[i]) === false) return elements
				} else {
				for (key in elements)
					if (callback.call(elements[key], key, elements[key]) === false) return elements
			}
			return elements
		},
		map:function(elements, callback){
			var value, values = [], i, key
			if (this.likeArray(elements))
			for (i = 0; i < elements.length; i++) {
				value = callback(elements[i], i)
				if (value != null) values.push(value)
			}
			else
			for (key in elements) {
				value = callback(elements[key], key)
				if (value != null) values.push(value)
			}
			return values.length > 0 ? WCJ.fn.concat.apply([], values) : values;
		},
		matches:function(element, selector){
			if (!selector || !element || element.nodeType !== 1) return false;
		    var matchesSelector = element.webkitMatchesSelector || element.mozMatchesSelector ||
		                          element.oMatchesSelector || element.msMatchesSelector || element.matchesSelector;
		    if (matchesSelector) return matchesSelector.call(element, selector);
		}
	});

	//解决低版本浏览器对filter方法的支持
	if (!Array.prototype.filter){
		Array.prototype.filter = function(fun /*, thisArg */){
			"use strict";
			if (this === void 0 || this === null)
				throw new TypeError();
			var t = Object(this);
			var len = t.length >>> 0;
			if (typeof fun !== "function")
				throw new TypeError();
			var res = [];
			var thisArg = arguments.length >= 2 ? arguments[1] : void 0;
			for (var i = 0; i < len; i++){
				if (i in t){
					var val = t[i];
					if (fun.call(thisArg, val, i, t))
					res.push(val);
				}
			}

			return res;
		};
	}

	//字符串处理
    WCJ.extend(String.prototype,{
        trim: function () {return this.replace(/(^\s*)|(\s*$)/g, "");},
        leftTrim: function () {return this.replace(/(^\s*)/g, "");}
    })
	window.WCJ = window.$$ = WCJ;
})(window);<|endoftext|>


<commit_before>/**
 * @file
 * Consent Controller.
 * This file contains the code for all the consent operations.
 */

var stepsProcess = require('./stepsProcess');
var request = require('request');
var async = require('async');
var consent = {};

consent.showConsent = function (req, res, next) {
    console.log('loading consent page');

    var config = req.app.get('config');
    var consentTransaction = req.session.consentTransaction;
    var consentData = {};
    consentData.application = consentTransaction.applicationname;
    var requestId = consentTransaction.request_id;
    var appId = consentTransaction.applicationid;
    var type = consentTransaction.scope;
    type = type.split(" ");
    type = type[0];
    consentData.tnc = consentTransaction.tandcs;

    var authenticationTransaction = req.session.authenticationTransaction;

    if (type == "accounts") {
        async.parallel({
            userAccounts: function (callback) {
                // Get the user Accounts
                var options = {
                    'url': config.getAccounts.transactionEndpoint + "?customerId=" + authenticationTransaction.user,
                    'method': config.getAccounts.method,
                    'headers': config.getAccounts.headers,
                    'json': true
                };
                request(options, callback);
            },
            accountRequest: function (callback) {
                // Fetch the Account Request
                var options = {
                    'url': config.getAccountRequest.transactionEndpoint + "/" + requestId,
                    'method': config.getAccountRequest.method,
                    'headers': config.getAccountRequest.headers,
                    'json': true
                };
                request(options, callback);

            }
        }, function (err, results) {
            // optional callback

            if (!err) {
                var accountRequestData = {}
                accountRequestData.tpp = consentData.application;
                var accountRequest = [];
                if (results.accountRequest[1] && results.accountRequest[1].AccountIds) {
                    accountRequest = results.accountRequest[1].AccountIds;
                    accountRequestData.permissions = results.accountRequest[1].Permissions;
                    accountRequestData.expiresAt = results.accountRequest[1].ExpiresAt;
                    accountRequestData.TransactionFromDateTime = results.accountRequest[1].TransactionFromDateTime;
                    accountRequestData.TransactionToDateTime = results.accountRequest[1].TransactionToDateTime;
                }
                var userAccounts = [];
                if (results.userAccounts[1]) {
                    userAccounts = results.userAccounts[1].Accounts;
                }
                var accounts = [];
                for (var i in userAccounts) {
                    var account = userAccounts[i];
                    account.requested = "";
                    for (var requestedAccount in accountRequest) {
                        if (accountRequest[requestedAccount] == account.AccountId) {
                            account.requested = "checked";
                        }
                    }
                    accounts.push(account)
                }

                accountRequestData.accounts = accounts;
                accountRequestData.cookies = req.cookies;
                res.render('account_consent', accountRequestData);
            }
            else {
                var error = {
                    "error": "NOT_FOUND",
                    "description": "Unable to fetch user account details"
                };
                stepsProcess.sendError(error, req, res, next);
            }

        });
    }
    else if (type == "payments") {
        async.parallel({
            userAccounts: function (callback) {
                // Get the user Accounts
                var options = {
                    'url': config.getAccounts.transactionEndpoint + "?customerId=" + authenticationTransaction.user,
                    'method': config.getAccounts.method,
                    'headers': config.getAccounts.headers,
                    'json': true
                };
                request(options, callback);
            },
            paymentRequest: function (callback) {
                // Fetch the Account Request
                var options = {
                    'url': config.getPaymentRequest.transactionEndpoint + "/" + requestId,
                    'method': config.getPaymentRequest.method,
                    'headers': config.getPaymentRequest.headers,
                    'json': true
                };
                request(options, callback);

            }
        }, function (err, results) {
            // optional callback

            if (!err) {
                var payementRequestData = {};
                payementRequestData.tpp = consentData.application;
                var paymentRequest = [];
                if (results.paymentRequest[1]) {
                    if (results.paymentRequest[1].InstructedAmount) {
                        payementRequestData.amount = results.paymentRequest[1].InstructedAmount.Amount;
                        payementRequestData.currency = results.paymentRequest[1].InstructedAmount.Currency;

                    }
                    if (results.paymentRequest[1].CreditorAgent && results.paymentRequest[1].CreditorAccount) {
                        payementRequestData.creditorAgentId = results.paymentRequest[1].CreditorAgent.Identification;
                        payementRequestData.creditorAccountId = results.paymentRequest[1].CreditorAccount.Identification;
                        payementRequestData.creditorName = results.paymentRequest[1].CreditorAccount.Name;

                    }
                }
                var userAccounts = [];
                if (results.userAccounts[1]) {
                    userAccounts = results.userAccounts[1].Accounts;
                }
                payementRequestData.debitorAccounts = userAccounts;
                payementRequestData.cookies = req.cookies;
                res.render('payment_consent', payementRequestData);
            }
            else {
                var error = {
                    "error": "NOT_FOUND",
                    "description": "Unable to fetch user account details"
                };
                stepsProcess.sendError(error, req, res, next);
            }
        });
    }
    else if (type = "openid") {
        var openidRequestData = {};
        openidRequestData.tpp = consentData.application;
        openidRequestData.permissions = consentTransaction.scope.split(" ");
        openidRequestData.permissions.shift();
        openidRequestData.cookies = req.cookies;
        res.render('openid_consent', openidRequestData);
    }

};

consent.doConsent = function (req, res, next) {
    // call the access token endpoint with the details of the user based on the
    // loa.
    // If the loa is just otp we need to send just the msisdn to the
    // backend, if the loa is username_password then the user information
    // needs to be sent

    var formData;
    var config = req.app.get('config');
    var consentTransaction = req.session.consentTransaction;
    var requestId = consentTransaction.request_id;
    var appId = consentTransaction.applicationid;
    var type = consentTransaction.scope;
    var customerId = req.session.authenticationTransaction.customer_id;
    var type = consentTransaction.scope;
    type = type.split(" ");
    type = type[0];
    // If the user allowed his consent the call the access token endpoint.
    if (req.body.allow == 'allow') {
        if (type == "accounts") {

            var consentedAccounts = [];//req.body.accounts;
            for (var i in req.body) {
                if (i.indexOf("accounts") != -1) {
                    if (!(req.body.accounts instanceof Array)) {
                        var account = JSON.parse(req.body.accounts);
                        consentedAccounts.push({"Account": account.Account, "Servicer": account.Servicer});
                    }
                    else {
                        for (var j in req.body.accounts) {
                            var account = JSON.parse(req.body.accounts[j]);
                            consentedAccounts.push({"Account": account.Account, "Servicer": account.Servicer});
                        }

                    }
                }
            }
            var options = {
                'url': config.updateAccountRequest.transactionEndpoint + "/" + requestId,
                'method': config.updateAccountRequest.method,
                'headers': config.updateAccountRequest.headers,
                'body': {
                    "SelectedAccounts": consentedAccounts,
                    "status": "Authorized"
                },
                'json': true
            };
            request(options, function (err, response, body) {
                if (!err && response.statusCode == 200) {
                    var options = {
                        'url': config.createConsent.transactionEndpoint,
                        'method': config.createConsent.method,
                        'headers': config.createConsent.headers,
                        'body': {
                            "requestId": requestId,
                            "customerId": customerId,
                            "appId": appId,
                            "consentType": type,
                            "browserInfo": req.headers['user-agent']
                        },
                        'json': true
                    };
                    request(options, function (error, response, body) {
                        if (!error && response.statusCode == 201) {
                            consent.getAccessToken(req, res, next, {"customerId": customerId, "type": type});
                        }
                        else {
                            var error = {
                                "error": "NOT_FOUND",
                                "description": "Unable to set Consent Info"
                            };
                            stepsProcess.sendError(error, req, res, next);
                        }

                    });
                }
                else {
                    var error = {
                        "error": "NOT_FOUND",
                        "description": "Unable to update account request Info"
                    };
                    stepsProcess.sendError(error, req, res, next);
                }

            });
        }
        else if (type == "payments") {
            var consentedPayment = JSON.parse(req.body.debitorAccount);
            DebtorAccountDetails = consentedPayment.Account;
            DebtorAgentDetails = consentedPayment.Servicer;
            var options = {
                'url': config.updatePaymentRequest.transactionEndpoint + "/" + requestId,
                'method': config.updatePaymentRequest.method,
                'headers': config.updatePaymentRequest.headers,
                'body': {
                    "DebtorAccount": DebtorAccountDetails,
                    "DebtorAgent": DebtorAgentDetails,
                    "status": "Authorized"
                },
                'json': true
            };
            request(options, function (err, response, body) {
                if (!err && response.statusCode == 200) {
                    var options = {
                        'url': config.createConsent.transactionEndpoint,
                        'method': config.createConsent.method,
                        'headers': config.createConsent.headers,
                        'body': {
                            "requestId": requestId,
                            "customerId": customerId,
                            "appId": appId,
                            "consentType": type,
                            "browserInfo": req.headers['user-agent']
                        },
                        'json': true
                    };
                    request(options, function (error, response, body) {
                        if (!error && response.statusCode == 201) {
                            consent.getAccessToken(req, res, next, {"customerId": customerId, "type": type});
                        }
                        else {
                            var error = {
                                "error": "NOT_FOUND",
                                "description": "Unable to set Consent Info"
                            };
                            stepsProcess.sendError(error, req, res, next);
                        }
                    });
                }
                else {
                    var error = {
                        "error": "NOT_FOUND",
                        "description": "Unable to update account request Info"
                    };
                    stepsProcess.sendError(error, req, res, next);
                }
            });
        }
        else if(type = "openid"){
            consent.getAccessToken(req, res, next, {"customerId": customerId, "type": type});
        }
    } else {
        // If the user rejects the consent then redirect to the application with
        // the error code and message.
        var config = req.app.get('config');
        var redirect_uri = req.session.consentTransaction.redirect_uri;
        redirect_uri += "?state=" + req.session.consentTransaction.state + "&error=" + config.errors.user_cancel.code + "&error_description=" + config.errors.user_cancel.description;
        req.session.destroy();
        res.redirect(redirect_uri);
    }

};

consent.getAccessToken = function (req, res, next, formData) {
    var config = req.app.get('config');

    // call the authentication endpoint to validate the user credentials
    var options = {
        'url': config.accessTokenTransaction.transactionEndpoint + req.session.sessionid,
        'method': config.accessTokenTransaction.method,
        'headers': config.accessTokenTransaction.headers,
        'body': formData,
        "json": true
    };

    request(options, function (error, response, body) {
        if (response.statusCode == 200) {
            var redirect_uri = body.application_tx_response;
            req.session.destroy();
            res.redirect(redirect_uri);
        } else {
            var err = {
                "error": body.error,
                "description": body.error_description
            };
            stepsProcess.sendError(err, req, res, next);
        }
    });
};

module.exports = consent;<commit_msg>Update consent.js<commit_after>/**
 * @file
 * Consent Controller.
 * This file contains the code for all the consent operations.
 */

var stepsProcess = require('./stepsProcess');
var request = require('request');
var async = require('async');
var consent = {};

consent.showConsent = function (req, res, next) {
    console.log('loading consent page');

    var config = req.app.get('config');
    var consentTransaction = req.session.consentTransaction;
    var consentData = {};
    consentData.application = consentTransaction.applicationname;
    var requestId = consentTransaction.request_id;
    var appId = consentTransaction.applicationid;
    var type = consentTransaction.scope;
    type = type.split(" ");
    type = type[0];
    consentData.tnc = consentTransaction.tandcs;

    var authenticationTransaction = req.session.authenticationTransaction;

    if (type == "accounts") {
        async.parallel({
            userAccounts: function (callback) {
                // Get the user Accounts
                var options = {
                    'url': config.getAccounts.transactionEndpoint + "?customerId=" + authenticationTransaction.user,
                    'method': config.getAccounts.method,
                    'headers': config.getAccounts.headers,
                    'json': true
                };
                request(options, callback);
            },
            accountRequest: function (callback) {
                // Fetch the Account Request
                var options = {
                    'url': config.getAccountRequest.transactionEndpoint + "/" + requestId,
                    'method': config.getAccountRequest.method,
                    'headers': config.getAccountRequest.headers,
                    'json': true
                };
                request(options, callback);

            }
        }, function (err, results) {
            // optional callback

            if (!err) {
                var accountRequestData = {}
                accountRequestData.tpp = consentData.application;
                var accountRequest = [];
                if (results.accountRequest[1] && results.accountRequest[1].AccountIds) {
                    accountRequest = results.accountRequest[1].AccountIds;
                    accountRequestData.permissions = results.accountRequest[1].Permissions;
                    accountRequestData.expiresAt = results.accountRequest[1].ExpiresAt;
                    accountRequestData.TransactionFromDateTime = results.accountRequest[1].TransactionFromDateTime;
                    accountRequestData.TransactionToDateTime = results.accountRequest[1].TransactionToDateTime;
                }
                var userAccounts = [];
                if (results.userAccounts[1]) {
                    userAccounts = results.userAccounts[1].Accounts;
                }
                var accounts = [];
                for (var i in userAccounts) {
                    var account = userAccounts[i];
                    account.requested = "";
                    for (var requestedAccount in accountRequest) {
                        if (accountRequest[requestedAccount] == account.AccountId) {
                            account.requested = "checked";
                        }
                    }
                    accounts.push(account)
                }

                accountRequestData.accounts = accounts;
                accountRequestData.cookies = req.cookies;
                res.render('account_consent', accountRequestData);
            }
            else {
                var error = {
                    "error": "NOT_FOUND",
                    "description": "Unable to fetch user account details"
                };
                stepsProcess.sendError(error, req, res, next);
            }

        });
    }
    else if (type == "payments") {
        async.parallel({
            userAccounts: function (callback) {
                // Get the user Accounts
                var options = {
                    'url': config.getAccounts.transactionEndpoint + "?customerId=" + authenticationTransaction.user,
                    'method': config.getAccounts.method,
                    'headers': config.getAccounts.headers,
                    'json': true
                };
                request(options, callback);
            },
            paymentRequest: function (callback) {
                // Fetch the Account Request
                var options = {
                    'url': config.getPaymentRequest.transactionEndpoint + "/" + requestId,
                    'method': config.getPaymentRequest.method,
                    'headers': config.getPaymentRequest.headers,
                    'json': true
                };
                request(options, callback);

            }
        }, function (err, results) {
            // optional callback

            if (!err) {
                var payementRequestData = {};
                payementRequestData.tpp = consentData.application;
                var paymentRequest = [];
                if (results.paymentRequest[1]) {
                    if (results.paymentRequest[1].InstructedAmount) {
                        payementRequestData.amount = results.paymentRequest[1].InstructedAmount.Amount;
                        payementRequestData.currency = results.paymentRequest[1].InstructedAmount.Currency;

                    }
                    if (results.paymentRequest[1].CreditorAgent && results.paymentRequest[1].CreditorAccount) {
                        payementRequestData.creditorAgentId = results.paymentRequest[1].CreditorAgent.Identification;
                        payementRequestData.creditorAccountId = results.paymentRequest[1].CreditorAccount.Identification;
                        payementRequestData.creditorName = results.paymentRequest[1].CreditorAccount.Name;

                    }
                }
                var userAccounts = [];
                if (results.userAccounts[1]) {
                    userAccounts = results.userAccounts[1].Accounts;
                }
                payementRequestData.debitorAccounts = userAccounts;
                payementRequestData.cookies = req.cookies;
                res.render('payment_consent', payementRequestData);
            }
            else {
                var error = {
                    "error": "NOT_FOUND",
                    "description": "Unable to fetch user account details"
                };
                stepsProcess.sendError(error, req, res, next);
            }
        });
    }
    else if (type = "openid") {
        var openidRequestData = {};
        openidRequestData.tpp = consentData.application;
        openidRequestData.permissions = consentTransaction.scope.split(" ");
        openidRequestData.permissions.shift();
        openidRequestData.cookies = req.cookies;
        res.render('openid_consent', openidRequestData);
    }

};

consent.doConsent = function (req, res, next) {
    // call the access token endpoint with the details of the user based on the
    // loa.
    // If the loa is just otp we need to send just the msisdn to the
    // backend, if the loa is username_password then the user information
    // needs to be sent

    var formData;
    var config = req.app.get('config');
    var consentTransaction = req.session.consentTransaction;
    var requestId = consentTransaction.request_id;
    var appId = consentTransaction.applicationid;
    var type = consentTransaction.scope;
    var customerId = req.session.authenticationTransaction.customer_id;
    var type = consentTransaction.scope;
    type = type.split(" ");
    type = type[0];
    // If the user allowed his consent the call the access token endpoint.
    if (req.body.allow == 'allow') {
        if (type == "accounts") {

            var consentedAccounts = [];//req.body.accounts;
            for (var i in req.body) {
                if (i.indexOf("accounts") != -1) {
                    if (!(req.body.accounts instanceof Array)) {
                        var account = JSON.parse(req.body.accounts);
                        consentedAccounts.push({"Account": account.Account, "Servicer": account.Servicer});
                    }
                    else {
                        for (var j in req.body.accounts) {
                            var account = JSON.parse(req.body.accounts[j]);
                            consentedAccounts.push({"Account": account.Account, "Servicer": account.Servicer});
                        }

                    }
                }
            }
            var options = {
                'url': config.updateAccountRequest.transactionEndpoint + "/" + requestId,
                'method': config.updateAccountRequest.method,
                'headers': config.updateAccountRequest.headers,
                'body': {
                    "SelectedAccounts": consentedAccounts,
                    "status": "Authorized"
                },
                'json': true
            };
            request(options, function (err, response, body) {
                if (!err && response.statusCode == 200) {
                    var options = {
                        'url': config.createConsent.transactionEndpoint,
                        'method': config.createConsent.method,
                        'headers': config.createConsent.headers,
                        'body': {
                            "requestId": requestId,
                            "customerId": customerId,
                            "appId": appId,
                            "consentType": type,
                            "browserInfo": req.headers['user-agent']
                        },
                        'json': true
                    };
                    request(options, function (error, response, body) {
                        if (!error && response.statusCode == 201) {
                            consent.getAccessToken(req, res, next, {"customerId": customerId, "type": type});
                        }
                        else {
                            var error = {
                                "error": "NOT_FOUND",
                                "description": "Unable to set Consent Info"
                            };
                            stepsProcess.sendError(error, req, res, next);
                        }

                    });
                }
                else {
                    var error = {
                        "error": "NOT_FOUND",
                        "description": "Unable to update account request Info"
                    };
                    stepsProcess.sendError(error, req, res, next);
                }

            });
        }
        else if (type == "payments") {
            var consentedPayment = JSON.parse(req.body.debitorAccount);
            DebtorAccountDetails = consentedPayment.Account;
            DebtorAgentDetails = consentedPayment.Servicer;
            var options = {
                'url': config.updatePaymentRequest.transactionEndpoint + "/" + requestId,
                'method': config.updatePaymentRequest.method,
                'headers': config.updatePaymentRequest.headers,
                'body': {
                    "DebtorAccount": DebtorAccountDetails,
                    "DebtorAgent": DebtorAgentDetails,
                    "status": "Authorized"
                },
                'json': true
            };
            request(options, function (err, response, body) {
                if (!err && response.statusCode == 200) {
                    var options = {
                        'url': config.createConsent.transactionEndpoint,
                        'method': config.createConsent.method,
                        'headers': config.createConsent.headers,
                        'body': {
                            "requestId": requestId,
                            "customerId": customerId,
                            "appId": appId,
                            "consentType": type,
                            "browserInfo": req.headers['user-agent']
                        },
                        'json': true
                    };
                    request(options, function (error, response, body) {
                        if (!error && response.statusCode == 201) {
                            consent.getAccessToken(req, res, next, {"customerId": customerId, "type": type});
                        }
                        else {
                            var error = {
                                "error": "NOT_FOUND",
                                "description": "Unable to set Consent Info"
                            };
                            stepsProcess.sendError(error, req, res, next);
                        }
                    });
                }
                else {
                    var error = {
                        "error": "NOT_FOUND",
                        "description": "Unable to update payment request Info"
                    };
                    stepsProcess.sendError(error, req, res, next);
                }
            });
        }
        else if(type = "openid"){
            consent.getAccessToken(req, res, next, {"customerId": customerId, "type": type});
        }
    } else {
        // If the user rejects the consent then redirect to the application with
        // the error code and message.
        var config = req.app.get('config');
        var redirect_uri = req.session.consentTransaction.redirect_uri;
        redirect_uri += "?state=" + req.session.consentTransaction.state + "&error=" + config.errors.user_cancel.code + "&error_description=" + config.errors.user_cancel.description;
        req.session.destroy();
        res.redirect(redirect_uri);
    }

};

consent.getAccessToken = function (req, res, next, formData) {
    var config = req.app.get('config');

    // call the authentication endpoint to validate the user credentials
    var options = {
        'url': config.accessTokenTransaction.transactionEndpoint + req.session.sessionid,
        'method': config.accessTokenTransaction.method,
        'headers': config.accessTokenTransaction.headers,
        'body': formData,
        "json": true
    };

    request(options, function (error, response, body) {
        if (response.statusCode == 200) {
            var redirect_uri = body.application_tx_response;
            req.session.destroy();
            res.redirect(redirect_uri);
        } else {
            var err = {
                "error": body.error,
                "description": body.error_description
            };
            stepsProcess.sendError(err, req, res, next);
        }
    });
};

module.exports = consent;
<|endoftext|>


<commit_before>/* eslint-disable
    camelcase,
    handle-callback-err,
    no-unused-vars,
*/
let ProjectManager
const RedisManager = require('./RedisManager')
const ProjectHistoryRedisManager = require('./ProjectHistoryRedisManager')
const DocumentManager = require('./DocumentManager')
const HistoryManager = require('./HistoryManager')
const async = require('async')
const logger = require('logger-sharelatex')
const Metrics = require('./Metrics')
const Errors = require('./Errors')

module.exports = ProjectManager = {
  flushProjectWithLocks(project_id, _callback) {
    const timer = new Metrics.Timer('projectManager.flushProjectWithLocks')
    const callback = function (...args) {
      timer.done()
      _callback(...args)
    }

    RedisManager.getDocIdsInProject(project_id, function (error, doc_ids) {
      if (error) {
        return callback(error)
      }
      const jobs = []
      const errors = []
      for (const doc_id of doc_ids) {
        ;((doc_id) =>
          jobs.push((callback) =>
            DocumentManager.flushDocIfLoadedWithLock(
              project_id,
              doc_id,
              function (error) {
                if (error instanceof Errors.NotFoundError) {
                  logger.warn(
                    { err: error, project_id, doc_id },
                    'found deleted doc when flushing'
                  )
                  callback()
                } else if (error) {
                  logger.error(
                    { err: error, project_id, doc_id },
                    'error flushing doc'
                  )
                  errors.push(error)
                  callback()
                } else {
                  callback()
                }
              }
            )
          ))(doc_id)
      }

      logger.log({ project_id, doc_ids }, 'flushing docs')
      async.series(jobs, function () {
        if (errors.length > 0) {
          callback(new Error('Errors flushing docs. See log for details'))
        } else {
          callback(null)
        }
      })
    })
  },

  flushAndDeleteProjectWithLocks(project_id, options, _callback) {
    const timer = new Metrics.Timer(
      'projectManager.flushAndDeleteProjectWithLocks'
    )
    const callback = function (...args) {
      timer.done()
      _callback(...args)
    }

    RedisManager.getDocIdsInProject(project_id, function (error, doc_ids) {
      if (error) {
        return callback(error)
      }
      const jobs = []
      const errors = []
      for (const doc_id of doc_ids) {
        ;((doc_id) =>
          jobs.push((callback) =>
            DocumentManager.flushAndDeleteDocWithLock(
              project_id,
              doc_id,
              {},
              function (error) {
                if (error) {
                  logger.error(
                    { err: error, project_id, doc_id },
                    'error deleting doc'
                  )
                  errors.push(error)
                }
                callback()
              }
            )
          ))(doc_id)
      }

      logger.log({ project_id, doc_ids }, 'deleting docs')
      async.series(jobs, () =>
        // When deleting the project here we want to ensure that project
        // history is completely flushed because the project may be
        // deleted in web after this call completes, and so further
        // attempts to flush would fail after that.
        HistoryManager.flushProjectChanges(project_id, options, function (
          error
        ) {
          if (errors.length > 0) {
            callback(new Error('Errors deleting docs. See log for details'))
          } else if (error) {
            callback(error)
          } else {
            callback(null)
          }
        })
      )
    })
  },

  queueFlushAndDeleteProject(project_id, callback) {
    RedisManager.queueFlushAndDeleteProject(project_id, function (error) {
      if (error) {
        logger.error(
          { project_id, error },
          'error adding project to flush and delete queue'
        )
        return callback(error)
      }
      Metrics.inc('queued-delete')
      callback()
    })
  },

  getProjectDocsTimestamps(project_id, callback) {
    RedisManager.getDocIdsInProject(project_id, function (error, doc_ids) {
      if (error) {
        return callback(error)
      }
      if (doc_ids.length === 0) {
        return callback(null, [])
      }
      RedisManager.getDocTimestamps(doc_ids, function (error, timestamps) {
        if (error) {
          return callback(error)
        }
        callback(null, timestamps)
      })
    })
  },

  getProjectDocsAndFlushIfOld(
    project_id,
    projectStateHash,
    excludeVersions,
    _callback
  ) {
    const timer = new Metrics.Timer(
      'projectManager.getProjectDocsAndFlushIfOld'
    )
    const callback = function (...args) {
      timer.done()
      _callback(...args)
    }

    RedisManager.checkOrSetProjectState(project_id, projectStateHash, function (
      error,
      projectStateChanged
    ) {
      if (error) {
        logger.error(
          { err: error, project_id },
          'error getting/setting project state in getProjectDocsAndFlushIfOld'
        )
        return callback(error)
      }
      // we can't return docs if project structure has changed
      if (projectStateChanged) {
        return callback(
          Errors.ProjectStateChangedError('project state changed')
        )
      }
      // project structure hasn't changed, return doc content from redis
      RedisManager.getDocIdsInProject(project_id, function (error, doc_ids) {
        if (error) {
          logger.error(
            { err: error, project_id },
            'error getting doc ids in getProjectDocs'
          )
          return callback(error)
        }
        const jobs = []
        for (const doc_id of doc_ids) {
          ;((doc_id) =>
            jobs.push((
              cb // get the doc lines from redis
            ) =>
              DocumentManager.getDocAndFlushIfOldWithLock(
                project_id,
                doc_id,
                function (err, lines, version) {
                  if (err) {
                    logger.error(
                      { err, project_id, doc_id },
                      'error getting project doc lines in getProjectDocsAndFlushIfOld'
                    )
                    return cb(err)
                  }
                  const doc = { _id: doc_id, lines, v: version } // create a doc object to return
                  cb(null, doc)
                }
              )
            ))(doc_id)
        }
        async.series(jobs, function (error, docs) {
          if (error) {
            return callback(error)
          }
          callback(null, docs)
        })
      })
    })
  },

  clearProjectState(project_id, callback) {
    RedisManager.clearProjectState(project_id, callback)
  },

  updateProjectWithLocks(
    project_id,
    projectHistoryId,
    user_id,
    docUpdates,
    fileUpdates,
    version,
    _callback
  ) {
    const timer = new Metrics.Timer('projectManager.updateProject')
    const callback = function (...args) {
      timer.done()
      _callback(...args)
    }

    const project_version = version
    let project_subversion = 0 // project versions can have multiple operations

    let project_ops_length = 0

    const handleDocUpdate = function (projectUpdate, cb) {
      const doc_id = projectUpdate.id
      projectUpdate.version = `${project_version}.${project_subversion++}`
      if (projectUpdate.docLines != null) {
        ProjectHistoryRedisManager.queueAddEntity(
          project_id,
          projectHistoryId,
          'doc',
          doc_id,
          user_id,
          projectUpdate,
          function (error, count) {
            project_ops_length = count
            cb(error)
          }
        )
      } else {
        DocumentManager.renameDocWithLock(
          project_id,
          doc_id,
          user_id,
          projectUpdate,
          projectHistoryId,
          function (error, count) {
            project_ops_length = count
            cb(error)
          }
        )
      }
    }

    const handleFileUpdate = function (projectUpdate, cb) {
      const file_id = projectUpdate.id
      projectUpdate.version = `${project_version}.${project_subversion++}`
      if (projectUpdate.url != null) {
        ProjectHistoryRedisManager.queueAddEntity(
          project_id,
          projectHistoryId,
          'file',
          file_id,
          user_id,
          projectUpdate,
          function (error, count) {
            project_ops_length = count
            cb(error)
          }
        )
      } else {
        ProjectHistoryRedisManager.queueRenameEntity(
          project_id,
          projectHistoryId,
          'file',
          file_id,
          user_id,
          projectUpdate,
          function (error, count) {
            project_ops_length = count
            cb(error)
          }
        )
      }
    }

    async.eachSeries(docUpdates, handleDocUpdate, function (error) {
      if (error) {
        return callback(error)
      }
      async.eachSeries(fileUpdates, handleFileUpdate, function (error) {
        if (error) {
          return callback(error)
        }
        if (
          HistoryManager.shouldFlushHistoryOps(
            project_ops_length,
            docUpdates.length + fileUpdates.length,
            HistoryManager.FLUSH_PROJECT_EVERY_N_OPS
          )
        ) {
          HistoryManager.flushProjectChangesAsync(project_id)
        }
        callback()
      })
    })
  },
}
<commit_msg>Decaf cleanup: remove unused variables<commit_after>/* eslint-disable
    camelcase,
    handle-callback-err,
*/
const RedisManager = require('./RedisManager')
const ProjectHistoryRedisManager = require('./ProjectHistoryRedisManager')
const DocumentManager = require('./DocumentManager')
const HistoryManager = require('./HistoryManager')
const async = require('async')
const logger = require('logger-sharelatex')
const Metrics = require('./Metrics')
const Errors = require('./Errors')

module.exports = {
  flushProjectWithLocks(project_id, _callback) {
    const timer = new Metrics.Timer('projectManager.flushProjectWithLocks')
    const callback = function (...args) {
      timer.done()
      _callback(...args)
    }

    RedisManager.getDocIdsInProject(project_id, function (error, doc_ids) {
      if (error) {
        return callback(error)
      }
      const jobs = []
      const errors = []
      for (const doc_id of doc_ids) {
        ;((doc_id) =>
          jobs.push((callback) =>
            DocumentManager.flushDocIfLoadedWithLock(
              project_id,
              doc_id,
              function (error) {
                if (error instanceof Errors.NotFoundError) {
                  logger.warn(
                    { err: error, project_id, doc_id },
                    'found deleted doc when flushing'
                  )
                  callback()
                } else if (error) {
                  logger.error(
                    { err: error, project_id, doc_id },
                    'error flushing doc'
                  )
                  errors.push(error)
                  callback()
                } else {
                  callback()
                }
              }
            )
          ))(doc_id)
      }

      logger.log({ project_id, doc_ids }, 'flushing docs')
      async.series(jobs, function () {
        if (errors.length > 0) {
          callback(new Error('Errors flushing docs. See log for details'))
        } else {
          callback(null)
        }
      })
    })
  },

  flushAndDeleteProjectWithLocks(project_id, options, _callback) {
    const timer = new Metrics.Timer(
      'projectManager.flushAndDeleteProjectWithLocks'
    )
    const callback = function (...args) {
      timer.done()
      _callback(...args)
    }

    RedisManager.getDocIdsInProject(project_id, function (error, doc_ids) {
      if (error) {
        return callback(error)
      }
      const jobs = []
      const errors = []
      for (const doc_id of doc_ids) {
        ;((doc_id) =>
          jobs.push((callback) =>
            DocumentManager.flushAndDeleteDocWithLock(
              project_id,
              doc_id,
              {},
              function (error) {
                if (error) {
                  logger.error(
                    { err: error, project_id, doc_id },
                    'error deleting doc'
                  )
                  errors.push(error)
                }
                callback()
              }
            )
          ))(doc_id)
      }

      logger.log({ project_id, doc_ids }, 'deleting docs')
      async.series(jobs, () =>
        // When deleting the project here we want to ensure that project
        // history is completely flushed because the project may be
        // deleted in web after this call completes, and so further
        // attempts to flush would fail after that.
        HistoryManager.flushProjectChanges(project_id, options, function (
          error
        ) {
          if (errors.length > 0) {
            callback(new Error('Errors deleting docs. See log for details'))
          } else if (error) {
            callback(error)
          } else {
            callback(null)
          }
        })
      )
    })
  },

  queueFlushAndDeleteProject(project_id, callback) {
    RedisManager.queueFlushAndDeleteProject(project_id, function (error) {
      if (error) {
        logger.error(
          { project_id, error },
          'error adding project to flush and delete queue'
        )
        return callback(error)
      }
      Metrics.inc('queued-delete')
      callback()
    })
  },

  getProjectDocsTimestamps(project_id, callback) {
    RedisManager.getDocIdsInProject(project_id, function (error, doc_ids) {
      if (error) {
        return callback(error)
      }
      if (doc_ids.length === 0) {
        return callback(null, [])
      }
      RedisManager.getDocTimestamps(doc_ids, function (error, timestamps) {
        if (error) {
          return callback(error)
        }
        callback(null, timestamps)
      })
    })
  },

  getProjectDocsAndFlushIfOld(
    project_id,
    projectStateHash,
    excludeVersions,
    _callback
  ) {
    const timer = new Metrics.Timer(
      'projectManager.getProjectDocsAndFlushIfOld'
    )
    const callback = function (...args) {
      timer.done()
      _callback(...args)
    }

    RedisManager.checkOrSetProjectState(project_id, projectStateHash, function (
      error,
      projectStateChanged
    ) {
      if (error) {
        logger.error(
          { err: error, project_id },
          'error getting/setting project state in getProjectDocsAndFlushIfOld'
        )
        return callback(error)
      }
      // we can't return docs if project structure has changed
      if (projectStateChanged) {
        return callback(
          Errors.ProjectStateChangedError('project state changed')
        )
      }
      // project structure hasn't changed, return doc content from redis
      RedisManager.getDocIdsInProject(project_id, function (error, doc_ids) {
        if (error) {
          logger.error(
            { err: error, project_id },
            'error getting doc ids in getProjectDocs'
          )
          return callback(error)
        }
        const jobs = []
        for (const doc_id of doc_ids) {
          ;((doc_id) =>
            jobs.push((
              cb // get the doc lines from redis
            ) =>
              DocumentManager.getDocAndFlushIfOldWithLock(
                project_id,
                doc_id,
                function (err, lines, version) {
                  if (err) {
                    logger.error(
                      { err, project_id, doc_id },
                      'error getting project doc lines in getProjectDocsAndFlushIfOld'
                    )
                    return cb(err)
                  }
                  const doc = { _id: doc_id, lines, v: version } // create a doc object to return
                  cb(null, doc)
                }
              )
            ))(doc_id)
        }
        async.series(jobs, function (error, docs) {
          if (error) {
            return callback(error)
          }
          callback(null, docs)
        })
      })
    })
  },

  clearProjectState(project_id, callback) {
    RedisManager.clearProjectState(project_id, callback)
  },

  updateProjectWithLocks(
    project_id,
    projectHistoryId,
    user_id,
    docUpdates,
    fileUpdates,
    version,
    _callback
  ) {
    const timer = new Metrics.Timer('projectManager.updateProject')
    const callback = function (...args) {
      timer.done()
      _callback(...args)
    }

    const project_version = version
    let project_subversion = 0 // project versions can have multiple operations

    let project_ops_length = 0

    const handleDocUpdate = function (projectUpdate, cb) {
      const doc_id = projectUpdate.id
      projectUpdate.version = `${project_version}.${project_subversion++}`
      if (projectUpdate.docLines != null) {
        ProjectHistoryRedisManager.queueAddEntity(
          project_id,
          projectHistoryId,
          'doc',
          doc_id,
          user_id,
          projectUpdate,
          function (error, count) {
            project_ops_length = count
            cb(error)
          }
        )
      } else {
        DocumentManager.renameDocWithLock(
          project_id,
          doc_id,
          user_id,
          projectUpdate,
          projectHistoryId,
          function (error, count) {
            project_ops_length = count
            cb(error)
          }
        )
      }
    }

    const handleFileUpdate = function (projectUpdate, cb) {
      const file_id = projectUpdate.id
      projectUpdate.version = `${project_version}.${project_subversion++}`
      if (projectUpdate.url != null) {
        ProjectHistoryRedisManager.queueAddEntity(
          project_id,
          projectHistoryId,
          'file',
          file_id,
          user_id,
          projectUpdate,
          function (error, count) {
            project_ops_length = count
            cb(error)
          }
        )
      } else {
        ProjectHistoryRedisManager.queueRenameEntity(
          project_id,
          projectHistoryId,
          'file',
          file_id,
          user_id,
          projectUpdate,
          function (error, count) {
            project_ops_length = count
            cb(error)
          }
        )
      }
    }

    async.eachSeries(docUpdates, handleDocUpdate, function (error) {
      if (error) {
        return callback(error)
      }
      async.eachSeries(fileUpdates, handleFileUpdate, function (error) {
        if (error) {
          return callback(error)
        }
        if (
          HistoryManager.shouldFlushHistoryOps(
            project_ops_length,
            docUpdates.length + fileUpdates.length,
            HistoryManager.FLUSH_PROJECT_EVERY_N_OPS
          )
        ) {
          HistoryManager.flushProjectChangesAsync(project_id)
        }
        callback()
      })
    })
  },
}
<|endoftext|>