/*

* Copyright 2010-2022 Ecsypno <http://www.ecsypno.com>
*
* This file is part of the Arachni Framework project and is subject to
* redistribution and commercial restrictions. Please see the Arachni Framework
* web site for more information on licensing and terms of use.
*/

//if( !window.onerror ) { // window.errors = []; // window.onerror = function() { // window.errors.push( arguments ) // }; //}

/*

* Allows the system to optimize DOM/JS/AJAX analysis by overriding JS prototypes
* and tracking things like bound events and timers.
*/

var _tokenDOMMonitor = _tokenDOMMonitor || {

// Signals that our custom monitoring overrides have already been installed
// for this document.
initialized:         false,

event_inheritance_limit: null,

// Keeps track of setTimeout() calls.
timeouts:            [],

// Don't include these elements in the `digest` computation.
exclude_tags_from_digest:        ['P'],

// Don't include these attributes in the `digest` computation.
exclude_attributes_from_digest:  ['data-arachni-id'],

// These elements are interesting enough to be considered by
// `elements_with_events` even if they don't have any events.
allowed_elements_without_events: {
    "a": true,
    "input": true,
    "textarea": true,
    "select": true,
    "form": true
},

// These elements are allowed to inherit events from their ancestors.
allowed_elements_with_inherited_events: {
    "a": true,
    "input": true,
    "textarea": true,
    "select": true,
    "form": true,
    "li": true,
    "span": true,
    "button": true
},

// These elements should not have events so ignore them.
elements_without_events: {
    "base" : true,
    "bdo" : true,
    "br" : true,
    "head" : true,
    "html" : true,
    "iframe" : true,
    "meta" : true,
    "param" : true,
    "script" : true,
    "style" : true,
    "title" : true,
    "link" : true,
    "hr" : true
},

// These events are valid for all elements.
universally_valid_events: {
    "click" : true,
    "dblclick" : true,
    "mousedown" : true,
    "mousemove" : true,
    "mouseout" : true,
    "mouseover" : true,
    "mouseup" : true
},

// Valid events for interesting elements, any other events will be ignored.
valid_events_per_element:{
    "body" : {
        "load": true
    },
    "form" : {
        "submit": true,
        "reset": true
    },
    "input" : {
        "select": true,
        "change": true,
        "focus": true,
        "blur": true,
        "keydown": true,
        "keypress": true,
        "keyup": true,
        "input": true
    },
    "textarea" : {
        "select": true,
        "change": true,
        "focus": true,
        "blur": true,
        "keydown": true,
        "keypress": true,
        "keyup": true,
        "input": true
    },
    "select" : {
        "change": true,
        "focus": true,
        "blur": true
    },
    "button" : {
        "focus": true,
        "blur": true
    },
    "label" : {
        "focus": true,
        "blur": true
    }
},

// Generally valid events for when there's no element tag name, like for
// Window and Document.
valid_events: {
    "click" : true,
    "dblclick" : true,
    "mousedown" : true,
    "mousemove" : true,
    "mouseout" : true,
    "mouseover" : true,
    "mouseup" : true,
    "load" : true,
    "submit" : true,
    "reset" : true,
    "select" : true,
    "change" : true,
    "focus" : true,
    "blur" : true,
    "keydown" : true,
    "keypress" : true,
    "keyup" : true,
    "input" : true
},

initialize: function ( event_inheritance_limit ) {
    if( _tokenDOMMonitor.initialized ) return;

    _tokenDOMMonitor.event_inheritance_limit = event_inheritance_limit;

    _tokenDOMMonitor.track_setTimeout();
    _tokenDOMMonitor.track_addEventListener();

    _tokenDOMMonitor.initialized = true
},

update_trackers: function () {
},

// Returns information about all DOM elements that have events, along with
// some that are interesting enough to be included even without them.
//
// @param   {Number}    offset
//  Start processing elements at the given offset.
// @param   {Number}    batch_size
//  Max amount of elements to be returned.
//  Helps keep Selenium response sizes low to keep RAM usage under control
//  for pages with a large number of elements with events.
elements_with_events: function ( offset, batch_size, tag_name_whitelist ) {
    tag_name_whitelist = tag_name_whitelist || [];

    var whitelist = {};
    for( var f = 0; f < tag_name_whitelist.length; f++ ) {
        whitelist[tag_name_whitelist[f]] = true;
    }

    var events_with_elements = [];
    var elements = document.getElementsByTagName("*");
    var length   = elements.length;

    var global_events = window._arachni_events || [];
    global_events = global_events.concat( document._arachni_events || [] );
    global_events = _tokenDOMMonitor.arrayUnique( global_events );

    // Keeps track of the amount of relevant elements (i.e. with events), to
    // help with the creation of the batch that should be returned based on
    // `offset` and `batch_size`.
    var relevant_element_index = 0;

    var event_inheritance_limit   = 0;

    for( var i = 0; i < length; i++ ) {
        var element = elements[i];

        var tag_name = element.tagName.toLowerCase();

        if( _tokenDOMMonitor.is_element_without_events( tag_name ) )
            continue;

        // Pass this element's events down to its descendants.
        _tokenDOMMonitor.bequeath_events( element );

        if( tag_name_whitelist.length > 0 && !whitelist[tag_name] ) continue;

        // Skip invisible elements.
        if( !_tokenDOMMonitor.is_visible( element ) ) continue;

        var e = {
            tag_name:   tag_name,
            events:     element._arachni_events || [],
            attributes: {}
        };

        // If we haven't reached the event bubbling depth limit and the
        // current element is allowed to have inherited events, merge them
        // with its own.
        if(
            (
                !_tokenDOMMonitor.event_inheritance_limit ||
                event_inheritance_limit < _tokenDOMMonitor.event_inheritance_limit
            ) && _tokenDOMMonitor.is_allowed_element_with_inherited_events( e.tag_name )
        ) {
            e.events = e.events.concat( element._arachni_inherited_events || [] );
            e.events = _tokenDOMMonitor.arrayUnique( e.events.concat( global_events ) );

            event_inheritance_limit++;
        }

        var attributes  = element.attributes;
        var attr_length = attributes.length;

        // Extract attributes and events from them.
        for( var j = 0; j < attr_length; j++ ){
            var attr_name = attributes[j].nodeName;

            // Extract events and handlers from attributes and set them as
            // element events -- but only if they are appropriate for the
            // element type.
            if( _tokenDOMMonitor.is_valid_event_for_element( tag_name, attr_name ) ) {
                e.events.push(
                    [
                        attr_name.replace( 'on', '' ),
                        attributes[j].nodeValue
                    ]
                )
            }

            e.attributes[attr_name] = attributes[j].nodeValue;
        }

        // No events and the element isn't interesting enough to be taken
        // into account without any, skip it.
        if( !_tokenDOMMonitor.is_allowed_element_without_event( e.tag_name ) &&
            e.events.length == 0 ) {
            continue
        }

        // Group events and their handlers by event type, instead of having
        // them as independent tuples; and while we're at it do some
        // normalization too.
        var grouped_events = {};
        for( var k = 0; k < e.events.length; k++ ) {
            var event         = e.events[k];
            var event_name    = event[0].replace( 'on', '' );
            var event_handler = event[1];

            // Event type not appropriate for element, we don't know why and
            // we don't care, we shouldn't waste resources on it.
            if( !_tokenDOMMonitor.is_valid_event_for_element( tag_name, event_name ) ) {
                continue;
            }

            grouped_events[event_name] = grouped_events[event_name] || [];
            grouped_events[event_name].push( event_handler.toString() );
        }
        e.events = grouped_events;

        // Increase the index for the batch of elements that can be returned.
        relevant_element_index += 1;

        // If the batch index reached the specified offset allow for the
        // batch to be created, otherwise ignore the element.
        if( offset && relevant_element_index <= offset ) continue;

        events_with_elements.push( e );

        // Batch size reached, send it on its way.
        if( batch_size && events_with_elements.length == batch_size )
            return events_with_elements;
    }

    // If we got here it means that the current batch didn't reach max size,
    // just send whatever we managed to collect.
    return events_with_elements;
},

// Digest used to determine whether or not a page has already been seen,
// based on the currently available events.
//
// Uses both elements and their DOM events and possible audit workload to
// determine the ID, as page snapshots should be retained both when further
// browser analysis can be performed and when new element audit workload
// (but possibly without any DOM relevance) is available.
event_digest: function () {
    var id       = '';
    var elements = _tokenDOMMonitor.elements_with_events();

    for( var i = 0; i < elements.length; i++ ) {
        var element    = elements[i];
        var element_id = '';

        switch( element.tag_name ) {
            case 'a':
                element_id += element.attributes.href;
                break;

            case 'input':
            case 'textarea':
            case 'select':
                element_id += element.attributes.name;
                break;

            case 'form':
                element_id += element.attributes.name;
                element_id += ',';
                element_id += element.attributes.action;
                break;
        }

        id += element.tag_name + ':' + element_id + ':' +
            Object.keys(element.events).join();
        id += '-';
    }

    id += 'cookies:' + _tokenDOMMonitor.cookie_names_csv();

    return _tokenDOMMonitor.hashCode( id ).toString();
},

cookie_names_csv: function () {
    var cookies = document.cookie.split(';');
    var csv = '';

    for( var i = 0; i < cookies.length; i++ ) {
        csv += cookies[i].split( '=' )[0];
    }

    return csv;
},

// Returns an Integer digest of the current DOM tree (i.e. node names and
// their attributes without text-nodes).
digest: function () {
    var elements = document.getElementsByTagName("*");
    var length   = elements.length;

    var digest = '';
    for( var i = 0; i < length; i++ ) {
        var element = elements[i];

        if( _tokenDOMMonitor.exclude_tags_from_digest.indexOf( element.tagName ) > -1 )
            continue;

        digest += '<' + element.tagName;

        var attributes  = element.attributes;
        var attr_length = attributes.length;

        for( var j = 0; j < attr_length; j++ ){
            if( _tokenDOMMonitor.exclude_attributes_from_digest.indexOf( attributes[j].nodeName ) > -1 )
                continue;

            digest += ' ' + attributes[j].nodeName + '=' + attributes[j].nodeValue;
        }
        digest += '>'
    }

    return _tokenDOMMonitor.hashCode( digest );
},

// Override setTimeout() so that we'll know to wait for it to be triggered
// during DOM analysis to provide sufficient coverage.
track_setTimeout: function () {
    var original_setTimeout = window.setTimeout;

    window.setTimeout = function() {
        arguments[1] = parseInt( arguments[1] );

        args = [];
        for( i = 0; i < arguments.length; i++ ) {
            args[i] = arguments[i];
        }
        args[0] = args[0].toString();

        _tokenDOMMonitor.timeouts.push( args );

        original_setTimeout.apply( this, arguments );
    };
},

// Overrides window.addEventListener and Node.prototype.addEventListener
// to intercept event binds so that we can keep track of them in order to
// optimize DOM analysis.
track_addEventListener: function () {
    // Override window.addEventListener
    var original_Window_addEventListener = window.addEventListener;

    window.addEventListener = function ( event, listener, useCapture ) {
        _tokenDOMMonitor.registerEvent( window, event, listener );
        original_Window_addEventListener.apply( window, [].slice.call( arguments ) );
    };

    // Override document.addEventListener
    var original_Document_addEventListener = document.addEventListener;

    document.addEventListener = function ( event, listener, useCapture ) {
        _tokenDOMMonitor.registerEvent( document, event, listener );
        original_Document_addEventListener.apply( document, [].slice.call( arguments ) );
    };

    // Override Node.prototype.addEventListener
    var original_Node_addEventListener = Node.prototype.addEventListener;

    Node.prototype.addEventListener = function ( event, listener, useCapture ) {
        _tokenDOMMonitor.registerEvent( this, event, listener );
        original_Node_addEventListener.apply( this, [].slice.call( arguments ) );
    };
},

// Passes down the element's events to its descendants.
bequeath_events: function( element ) {
    var children = element.childNodes;

    for( var i = 0; i < children.length; i++ ) {
        var child = children[i];

        if( !('_arachni_inherited_events' in child) ) child['_arachni_inherited_events'] = [];

        // Merge the element's events with the child's existing inherited ones.
        if( element['_arachni_events'] ) {
            child['_arachni_inherited_events'] =
                element['_arachni_events'].concat( child['_arachni_inherited_events'] );
        }

        // Merge the element's inherited events with the child's existing
        // inherited ones.
        if( element['_arachni_inherited_events'] ) {
            child['_arachni_inherited_events'] =
                element['_arachni_inherited_events'].concat( child['_arachni_inherited_events'] );
        }

        // Make sure we didn't add duplicates.
        child['_arachni_inherited_events'] =
            _tokenDOMMonitor.arrayUnique( child['_arachni_inherited_events'] )
    }
},

arrayUnique: function( array ) {
    var a = array.concat();

    for( var i = 0; i < a.length; ++i ) {
        for( var j = i + 1; j < a.length; ++j ) {
            if( a[i] === a[j] )
                a.splice( j--, 1 );
        }
    }

    return a;
},

// Registers an event and its handler for the given element.
registerEvent: function ( element, event, handler ) {
    if( !('_arachni_events' in element) ) element['_arachni_events'] = [];

    // Custom events are usually in the form of "click.delegateEventsview13".
    event = event.split( '.' )[0];

    // There's no tag name (so we're dealing with Window or Document) and
    // the event doesn't generally look valid, bail out.
    if( !element.tagName && !_tokenDOMMonitor.is_valid_event( event ) )
        return;

    // There's a tag name but the event isn't valid for the element type,
    // bail out.
    if(
        element.tagName && !_tokenDOMMonitor.is_valid_event_for_element(
        element.tagName.toLowerCase(), event
    )
        ) {
        return;
    }

    // All is well, register the event with the element.
    element['_arachni_events'].push( [event, handler.toString()] );
},

// Sets a unique enough custom ID attribute to elements that lack proper IDs.
// This gets called externally (by the Browser) once the page is settled.
setElementIds: function() {
    var elements = document.getElementsByTagName("*");
    var length   = elements.length;

    for( var i = 0; i < length; i++ ) {
        var element = elements[i];

        // Window and others don't have attributes.
        if( typeof( element.getAttribute ) !== 'function' ||
            typeof( element.setAttribute) !== 'function' ) continue;

        // If the element has an ID we're cool, move on.
        if( element.getAttribute('id') ) continue;

        // Skip invisible elements.
        if( element.offsetWidth <= 0 && element.offsetHeight <= 0 ) continue;

        // We don't care about elements without events.
        if( !element._arachni_events || element._arachni_events.length == 0 ) continue;

        element.setAttribute( 'data-arachni-id', _tokenDOMMonitor.hashCode( element.innerHTML ) );
    }
},

is_visible: function( element ) {
    return !!(
        element.offsetWidth  ||
        element.offsetHeight ||
        element.getClientRects().length
    );
},

is_valid_event: function ( event ) {
    return Object.prototype.hasOwnProperty.call(
        _tokenDOMMonitor.valid_events,
        event.replace( 'on', '' )
    );
},

is_valid_event_for_element: function ( tag, event ) {
    event = event.replace( 'on', '' );

    if( Object.prototype.hasOwnProperty.call(
        _tokenDOMMonitor.universally_valid_events,
        event )){ return true; }

    return _tokenDOMMonitor.valid_events_per_element[tag] &&
        Object.prototype.hasOwnProperty.call(
            _tokenDOMMonitor.valid_events_per_element[tag],
            event
        )
},

is_allowed_element_without_event: function ( tag_name ) {
    return Object.prototype.hasOwnProperty.call(
        _tokenDOMMonitor.allowed_elements_without_events,
        tag_name
    );
},

is_allowed_element_with_inherited_events: function ( tag_name ) {
    return Object.prototype.hasOwnProperty.call(
        _tokenDOMMonitor.allowed_elements_with_inherited_events,
        tag_name
    );
},

is_element_without_events: function ( tag_name ) {
    return Object.prototype.hasOwnProperty.call(
        _tokenDOMMonitor.elements_without_events,
        tag_name
    );
},

hashCode: function( str ) {
    var hash = 0;
    if( str.length == 0 ) return hash;

    for( var i = 0; i < str.length; i++ ) {
        var char = str.charCodeAt( i );
        hash = ((hash << 5) - hash) + char;
        hash = hash & hash; // Convert to 32bit integer
    }

    return hash;
}

};