File size: 21,386 Bytes
046723b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
$(document).ready(function () {

    var browsersteps_session_id;
    var browser_interface_seconds_remaining = 0;
    var apply_buttons_disabled = false;
    var include_text_elements = $("#include_text_elements");
    var xpath_data = false;
    var current_selected_i;
    var state_clicked = false;
    var c;

    // redline highlight context
    var ctx;
    var last_click_xy = {'x': -1, 'y': -1}

    $(window).resize(function () {
        set_scale();
    });
    // Should always be disabled
    $('#browser_steps-0-operation option[value="Goto site"]').prop("selected", "selected");
    $('#browser_steps-0-operation').attr('disabled', 'disabled');

    $('#browsersteps-click-start').click(function () {
        $("#browsersteps-click-start").fadeOut();
        $("#browsersteps-selector-wrapper .spinner").fadeIn();
        start();
    });

    $('a#browsersteps-tab').click(function () {
        reset();
    });

    window.addEventListener('hashchange', function () {
        if (window.location.hash == '#browser-steps') {
            reset();
        }
    });

    function reset() {
        xpath_data = false;
        $('#browsersteps-img').removeAttr('src');
        $("#browsersteps-click-start").show();
        $("#browsersteps-selector-wrapper .spinner").hide();
        browser_interface_seconds_remaining = 0;
        browsersteps_session_id = false;
        apply_buttons_disabled = false;
        ctx.clearRect(0, 0, c.width, c.height);
        set_first_gotosite_disabled();
    }

    function set_first_gotosite_disabled() {
        $('#browser_steps >li:first-child select').val('Goto site').attr('disabled', 'disabled');
        $('#browser_steps >li:first-child').css('opacity', '0.5');
    }

    // Show seconds remaining until the browser interface needs to restart the session
    // (See comment at the top of changedetectionio/blueprint/browser_steps/__init__.py )
    setInterval(() => {
        if (browser_interface_seconds_remaining >= 1) {
            document.getElementById('browser-seconds-remaining').innerText = browser_interface_seconds_remaining + " seconds remaining in session";
            browser_interface_seconds_remaining -= 1;
        }
    }, "1000")


    function set_scale() {

        // some things to check if the scaling doesnt work
        // - that the widths/sizes really are about the actual screen size cat elements.json |grep -o width......|sort|uniq
        selector_image = $("img#browsersteps-img")[0];
        selector_image_rect = selector_image.getBoundingClientRect();

        // make the canvas and input steps the same size as the image
        $('#browsersteps-selector-canvas').attr('height', selector_image_rect.height).attr('width', selector_image_rect.width);
        //$('#browsersteps-selector-wrapper').attr('width', selector_image_rect.width);
        $('#browser-steps-ui').attr('width', selector_image_rect.width);

        x_scale = selector_image_rect.width / xpath_data['browser_width'];
        y_scale = selector_image_rect.height / selector_image.naturalHeight;
        ctx.strokeStyle = 'rgba(255,0,0, 0.9)';
        ctx.fillStyle = 'rgba(255,0,0, 0.1)';
        ctx.lineWidth = 3;
        console.log("scaling set  x: " + x_scale + " by y:" + y_scale);
    }

    // bootstrap it, this will trigger everything else
    $('#browsersteps-img').bind('load', function () {
        $('body').addClass('full-width');
        console.log("Loaded background...");

        document.getElementById("browsersteps-selector-canvas");
        c = document.getElementById("browsersteps-selector-canvas");
        // redline highlight context
        ctx = c.getContext("2d");
        // @todo is click better?
        $('#browsersteps-selector-canvas').off("mousemove mousedown click");
        // Undo disable_browsersteps_ui
        $("#browser-steps-ui").css('opacity', '1.0');

        // init
        set_scale();

        // @todo click ? some better library?
        $('#browsersteps-selector-canvas').bind('click', function (e) {
            // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent
            e.preventDefault()
        });

        // When the mouse moves we know which element it should be above
        // mousedown will link that to the UI (select the right action, highlight etc)
        $('#browsersteps-selector-canvas').bind('mousedown', function (e) {
            // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent
            e.preventDefault()
            last_click_xy = {'x': parseInt((1 / x_scale) * e.offsetX), 'y': parseInt((1 / y_scale) * e.offsetY)}
            process_selected(current_selected_i);
            current_selected_i = false;

            // if process selected returned false, then best we can do is offer a x,y click :(
            if (!found_something) {
                var first_available = $("ul#browser_steps li.empty").first();
                $('select', first_available).val('Click X,Y').change();
                $('input[type=text]', first_available).first().val(last_click_xy['x'] + ',' + last_click_xy['y']);
                draw_circle_on_canvas(e.offsetX, e.offsetY);
            }
        });

        // Debounce and find the current most 'interesting' element we are hovering above
        $('#browsersteps-selector-canvas').bind('mousemove', function (e) {
            if (!xpath_data) {
                return;
            }

            // checkbox if find elements is enabled
            ctx.clearRect(0, 0, c.width, c.height);
            ctx.fillStyle = 'rgba(255,0,0, 0.1)';
            ctx.strokeStyle = 'rgba(255,0,0, 0.9)';

            // Add in offset
            if ((typeof e.offsetX === "undefined" || typeof e.offsetY === "undefined") || (e.offsetX === 0 && e.offsetY === 0)) {
                var targetOffset = $(e.target).offset();
                e.offsetX = e.pageX - targetOffset.left;
                e.offsetY = e.pageY - targetOffset.top;
            }
            current_selected_i = false;
            // Reverse order - the most specific one should be deeper/"laster"
            // Basically, find the most 'deepest'
            var possible_elements = [];
            xpath_data['size_pos'].forEach(function (item, index) {
                // If we are in a bounding-box
                if (e.offsetY > item.top * y_scale && e.offsetY < item.top * y_scale + item.height * y_scale
                    &&
                    e.offsetX > item.left * y_scale && e.offsetX < item.left * y_scale + item.width * y_scale

                ) {
                    // Ignore really large ones, because we are scraping 'div' also from xpath_element_scraper but
                    // that div or whatever could be some wrapper and would generally make you select the whole page
                    if (item.width > 800 && item.height > 400) {
                        return
                    }

                    // There could be many elements here, record them all and then we'll find out which is the most 'useful'
                    // (input, textarea, button, A etc)
                    if (item.width < xpath_data['browser_width']) {
                        possible_elements.push(item);
                    }
                }
            });

            // Find the best one
            if (possible_elements.length) {
                possible_elements.forEach(function (item, index) {
                  if (["a", "input", "textarea", "button"].includes(item['tagName'])) {
                      current_selected_i = item;
                  }
                });

                if (!current_selected_i) {
                    current_selected_i = possible_elements[0];
                }

                sel = xpath_data['size_pos'][current_selected_i];
                ctx.strokeRect(current_selected_i.left * x_scale, current_selected_i.top * y_scale, current_selected_i.width * x_scale, current_selected_i.height * y_scale);
                ctx.fillRect(current_selected_i.left * x_scale, current_selected_i.top * y_scale, current_selected_i.width * x_scale, current_selected_i.height * y_scale);
            }


        }.debounce(10));
    });

//    $("#browser-steps-fieldlist").bind('mouseover', function(e) {
//        console.log(e.xpath_data_index);
    // });


    // callback for clicking on an xpath on the canvas
    function process_selected(selected_in_xpath_list) {
        found_something = false;
        var first_available = $("ul#browser_steps li.empty").first();


        if (selected_in_xpath_list !== false) {
            // Nothing focused, so fill in a new one
            // if inpt type button or <button>
            // from the top, find the next not used one and use it
            var x = selected_in_xpath_list;
            console.log(x);
            if (x && first_available.length) {
                // @todo will it let you click shit that has a layer ontop? probably not.
                if (x['tagtype'] === 'text' || x['tagtype'] === 'number' || x['tagtype'] === 'email' || x['tagName'] === 'textarea' || x['tagtype'] === 'password' || x['tagtype'] === 'search') {
                    $('select', first_available).val('Enter text in field').change();
                    $('input[type=text]', first_available).first().val(x['xpath']);
                    $('input[placeholder="Value"]', first_available).addClass('ok').click().focus();
                    found_something = true;
                }
                else if (x['tagName'] === 'select') {
                    $('select', first_available).val('<select> by option text').change();
                    $('input[type=text]', first_available).first().val(x['xpath']);
                    $('input[placeholder="Value"]', first_available).addClass('ok').click().focus();
                    found_something = true;
                }
                else {
                    // There's no good way (that I know) to find if this
                    // see https://stackoverflow.com/questions/446892/how-to-find-event-listeners-on-a-dom-node-in-javascript-or-in-debugging
                    // https://codepen.io/azaslavsky/pen/DEJVWv

                    // So we dont know if its really a clickable element or not :-(
                    // Assume it is - then we dont fill the pages with unreliable "Click X,Y" selections
                    // If you switch to "Click X,y" after an element here is setup, it will give the last co-ords anyway
                    //if (x['isClickable'] || x['tagName'].startsWith('h') || x['tagName'] === 'a' || x['tagName'] === 'button' || x['tagtype'] === 'submit' || x['tagtype'] === 'checkbox' || x['tagtype'] === 'radio' || x['tagtype'] === 'li') {
                        $('select', first_available).val('Click element').change();
                        $('input[type=text]', first_available).first().val(x['xpath']).focus();
                        found_something = true;
                    //}
                }
            }
        }
    }

    function draw_circle_on_canvas(x, y) {
        ctx.beginPath();
        ctx.arc(x, y, 8, 0, 2 * Math.PI, false);
        ctx.fillStyle = 'rgba(255,0,0, 0.6)';
        ctx.fill();
    }

    function start() {
        console.log("Starting browser-steps UI");
        browsersteps_session_id = false;
        // @todo This setting of the first one should be done at the datalayer but wtforms doesnt wanna play nice
        $('#browser_steps >li:first-child').removeClass('empty');
        set_first_gotosite_disabled();
        $('#browser-steps-ui .loader .spinner').show();
        $('.clear,.remove', $('#browser_steps >li:first-child')).hide();
        $.ajax({
            type: "GET",
            url: browser_steps_start_url,
            statusCode: {
                400: function () {
                    // More than likely the CSRF token was lost when the server restarted
                    alert("There was a problem processing the request, please reload the page.");
                },
                401: function (err) {
                    // This will be a custom error
                    alert(err.responseText);
                }
            }
        }).done(function (data) {
            $("#loading-status-text").fadeIn();
            browsersteps_session_id = data.browsersteps_session_id;
            // This should trigger 'Goto site'
            console.log("Got startup response, requesting Goto-Site (first) step fake click");
            $('#browser_steps >li:first-child .apply').click();
            browser_interface_seconds_remaining = 500;
            set_first_gotosite_disabled();
        }).fail(function (data) {
            console.log(data);
            alert('There was an error communicating with the server.');
        });

    }

    function disable_browsersteps_ui() {
        set_first_gotosite_disabled();
        $("#browser-steps-ui").css('opacity', '0.3');
        $('#browsersteps-selector-canvas').off("mousemove mousedown click");
    }


    ////////////////////////// STEPS UI ////////////////////
    $('ul#browser_steps [type="text"]').keydown(function (e) {
        if (e.keyCode === 13) {
            // hitting [enter] in a browser-step input should trigger the 'Apply'
            e.preventDefault();
            $(".apply", $(this).closest('li')).click();
            return false;
        }
    });

    // Look up which step was selected, and enable or disable the related extra fields
    // So that people using it dont' get confused
    $('ul#browser_steps select').on("change", function () {
        var config = browser_steps_config[$(this).val()].split(' ');
        var elem_selector = $('tr:nth-child(2) input', $(this).closest('tbody'));
        var elem_value = $('tr:nth-child(3) input', $(this).closest('tbody'));

        if (config[0] == 0) {
            $(elem_selector).fadeOut();
        } else {
            $(elem_selector).fadeIn();
        }
        if (config[1] == 0) {
            $(elem_value).fadeOut();
        } else {
            $(elem_value).fadeIn();
        }

        if ($(this).val() === 'Click X,Y' && last_click_xy['x'] > 0 && $(elem_value).val().length === 0) {
            // @todo handle scale
            $(elem_value).val(last_click_xy['x'] + ',' + last_click_xy['y']).focus();
        }
    }).change();

    function set_greyed_state() {
        $('ul#browser_steps select').not('option:selected[value="Choose one"]').closest('li').removeClass('empty');
        $('ul#browser_steps select option:selected[value="Choose one"]').closest('li').addClass('empty');
    }

    // Add the extra buttons to the steps
    $('ul#browser_steps li').each(function (i) {
            var s = '<div class="control">' + '<a data-step-index=' + i + ' class="pure-button button-secondary button-green button-xsmall apply" >Apply</a>&nbsp;';
            if (i > 0) {
                // The first step never gets these (Goto-site)
                s += `<a data-step-index="${i}" class="pure-button button-secondary button-xsmall clear" >Clear</a>&nbsp;` +
                    `<a data-step-index="${i}" class="pure-button button-secondary button-red button-xsmall remove" >Remove</a>`;

                // if a screenshot is available
                if (browser_steps_available_screenshots.includes(i.toString())) {
                    var d = (browser_steps_last_error_step === i+1) ? 'before' : 'after';
                    s += `&nbsp;<a data-step-index="${i}" class="pure-button button-secondary button-xsmall show-screenshot" title="Show screenshot from last run" data-type="${d}">Pic</a>&nbsp;`;
                }
            }
            s += '</div>';
            $(this).append(s)
        }
    );

    $('ul#browser_steps li .control .clear').click(function (element) {
        $("select", $(this).closest('li')).val("Choose one").change();
        $(":text", $(this).closest('li')).val('');
    });


    $('ul#browser_steps li .control .remove').click(function (element) {
        // so you wanna remove the 2nd (3rd spot 0,1,2,...)
        var p = $("#browser_steps li").index($(this).closest('li'));

        var elem_to_remove = $("#browser_steps li")[p];
        $('.clear', elem_to_remove).click();
        $("#browser_steps li").slice(p, 10).each(function (index) {
            // get the next one's value from where we clicked
            var next = $("#browser_steps li")[p + index + 1];
            if (next) {
                // and set THIS ones value from the next one
                var n = $('input', next);
                $("select", $(this)).val($('select', next).val());
                $('input', this)[0].value = $(n)[0].value;
                $('input', this)[1].value = $(n)[1].value;
                // Triggers reconfiguring the field based on the system config
                $("select", $(this)).change();
            }

        });

        // Reset their hidden/empty states
        set_greyed_state();
    });

    $('ul#browser_steps li .control .apply').click(function (event) {
        // sequential requests @todo refactor
        if (apply_buttons_disabled) {
            return;
        }

        var current_data = $(event.currentTarget).closest('li');
        $('#browser-steps-ui .loader .spinner').fadeIn();
        apply_buttons_disabled = true;
        $('ul#browser_steps li .control .apply').css('opacity', 0.5);
        $("#browsersteps-img").css('opacity', 0.65);

        var is_last_step = 0;
        var step_n = $(event.currentTarget).data('step-index');

        // On the last step, we should also be getting data ready for the visual selector
        $('ul#browser_steps li select').each(function (i) {
            if ($(this).val() !== 'Choose one') {
                is_last_step += 1;
            }
        });

        if (is_last_step == (step_n + 1)) {
            is_last_step = true;
        } else {
            is_last_step = false;
        }

        console.log("Requesting step via POST " + $("select[id$='operation']", current_data).first().val());
        // POST the currently clicked step form widget back and await response, redraw
        $.ajax({
            method: "POST",
            url: browser_steps_sync_url + "&browsersteps_session_id=" + browsersteps_session_id,
            data: {
                'operation': $("select[id$='operation']", current_data).first().val(),
                'selector': $("input[id$='selector']", current_data).first().val(),
                'optional_value': $("input[id$='optional_value']", current_data).first().val(),
                'step_n': step_n,
                'is_last_step': is_last_step
            },
            statusCode: {
                400: function () {
                    // More than likely the CSRF token was lost when the server restarted
                    alert("There was a problem processing the request, please reload the page.");
                    $("#loading-status-text").hide();
                    $('#browser-steps-ui .loader .spinner').fadeOut();
                },
                401: function (data) {
                    // More than likely the CSRF token was lost when the server restarted
                    alert(data.responseText);
                    $("#loading-status-text").hide();
                    $('#browser-steps-ui .loader .spinner').fadeOut();
                }
            }
        }).done(function (data) {
            // it should return the new state (selectors available and screenshot)
            xpath_data = data.xpath_data;
            $('#browsersteps-img').attr('src', data.screenshot);
            $('#browser-steps-ui .loader .spinner').fadeOut();
            apply_buttons_disabled = false;
            $("#browsersteps-img").css('opacity', 1);
            $('ul#browser_steps li .control .apply').css('opacity', 1);
            $("#loading-status-text").hide();
            set_first_gotosite_disabled();
        }).fail(function (data) {
            console.log(data);
            if (data.responseText.includes("Browser session expired")) {
                disable_browsersteps_ui();
            }
            apply_buttons_disabled = false;
            $("#loading-status-text").hide();
            $('ul#browser_steps li .control .apply').css('opacity', 1);
            $("#browsersteps-img").css('opacity', 1);
        });

    });

    $('ul#browser_steps li .control .show-screenshot').click(function (element) {
        var step_n = $(event.currentTarget).data('step-index');
        w = window.open(this.href, "_blank", "width=640,height=480");
        const t = $(event.currentTarget).data('type');

        const url = browser_steps_fetch_screenshot_image_url + `&step_n=${step_n}&type=${t}`;
        w.document.body.innerHTML = `<!DOCTYPE html>
            <html lang="en">
                <body>
                    <img src="${url}" style="width: 100%" alt="Browser Step at step ${step_n} from last run." title="Browser Step at step ${step_n} from last run."/>
                </body>
        </html>`;
        w.document.title = `Browser Step at step ${step_n} from last run.`;
    });

    if (browser_steps_last_error_step) {
        $("ul#browser_steps>li:nth-child("+browser_steps_last_error_step+")").addClass("browser-step-with-error");
    }

    $("ul#browser_steps select").change(function () {
        set_greyed_state();
    }).change();

});