From 7338adaef330f9caf734b8a8e586fc980b34d256 Mon Sep 17 00:00:00 2001 From: Gertjan Date: Thu, 24 Oct 2024 15:46:12 +0000 Subject: [PATCH 01/57] Store all requests to pages hosted by BugHog and minor fixes --- bci/evaluations/collectors/base.py | 21 ++++--------- bci/evaluations/collectors/requests.py | 41 +++++++++++++++++++------- bci/web/blueprints/experiments.py | 25 ++++++---------- bci/web/templates/cookies.html | 13 +++++--- nginx/config/experiments.conf | 10 +++++++ nginx/config/notify_collector.conf | 30 +++++++++++++++++++ 6 files changed, 93 insertions(+), 47 deletions(-) create mode 100644 nginx/config/notify_collector.conf diff --git a/bci/evaluations/collectors/base.py b/bci/evaluations/collectors/base.py index 903ed5d2..b85ee6f5 100644 --- a/bci/evaluations/collectors/base.py +++ b/bci/evaluations/collectors/base.py @@ -3,23 +3,22 @@ class BaseCollector: - def __init__(self) -> None: self.data = {} @abstractmethod - def start(): + def start(self): pass @abstractmethod - def stop(): + def stop(self): pass @staticmethod def _parse_bughog_variables(raw_log_lines: list[str], regex) -> list[tuple[str, str]]: - ''' + """ Parses the given `raw_log_lines` for matches against the given `regex`. - ''' + """ data = [] regex_match_lists = [re.findall(regex, line) for line in raw_log_lines if re.search(regex, line)] # Flatten list @@ -27,15 +26,5 @@ def _parse_bughog_variables(raw_log_lines: list[str], regex) -> list[tuple[str, for match in regex_matches: var = match[0] val = match[1] - BaseCollector._add_val_var_pair(var, val, data) + data.append({'var': var, 'val': val}) return data - - @staticmethod - def _add_val_var_pair(var: str, val: str, data: list) -> list: - for entry in data: - if entry['var'] == var and entry['val'] == val: - return data - data.append({ - 'var': var, - 'val': val - }) diff --git a/bci/evaluations/collectors/requests.py b/bci/evaluations/collectors/requests.py index d5b9efe3..1e6906ba 100644 --- a/bci/evaluations/collectors/requests.py +++ b/bci/evaluations/collectors/requests.py @@ -1,6 +1,7 @@ import http.server import json import logging +import socket import socketserver from threading import Thread @@ -12,32 +13,49 @@ class RequestHandler(http.server.BaseHTTPRequestHandler): + """ + Handles requests sent to the collector. + """ def __init__(self, collector, request, client_address, server) -> None: self.collector = collector self.request_body = None super().__init__(request, client_address, server) - def log_message(self, *_): + def log_message(self, format: str, *args) -> None: + """ + Handle and store the received body. + """ if not self.request_body: logger.debug('Received request without body') return - logger.debug(f'Received request with body: {self.request_body}') request_body = json.loads(self.request_body) + logger.debug(f'Received request information with {len(request_body.keys())} attributes.') self.collector.data['requests'].append(request_body) def do_POST(self): - content_length = int(self.headers['Content-Length']) - body = self.rfile.read(content_length) - self.request_body = body.decode('utf-8') - self.send_response(200) - self.end_headers() - self.wfile.write(b'Post request received') + """ + This function is called upon receiving a POST request. + It sets `self.request_body`, which will be parsed later by `self.log_message`. + """ + # We have to read the body before allowing it to be thrashed when connection clusure is confirmed. + if self.headers['Content-Length'] is not None: + content_length = int(self.headers['Content-Length']) + body = self.rfile.read(content_length) + self.request_body = body.decode('utf-8') + # Because of our hacky NGINX methodology, we have to allow premature socket closings. + try: + self.send_response(200) + self.send_header('Content-Type', 'text/plain; charset=utf-8') + self.end_headers() + self.wfile.write('Post request received!\n'.encode('utf-8')) + except socket.error: + logger.debug('Socket closed by NGINX (expected)') -class RequestCollector(BaseCollector): +class RequestCollector(BaseCollector): def __init__(self): super().__init__() self.__httpd = None @@ -48,7 +66,7 @@ def __init__(self): def start(self): logger.debug('Starting collector...') socketserver.TCPServer.allow_reuse_address = True - self.__httpd = socketserver.TCPServer(("", PORT), lambda *args, **kwargs: RequestHandler(self, *args, **kwargs)) + self.__httpd = socketserver.TCPServer(('', PORT), lambda *args, **kwargs: RequestHandler(self, *args, **kwargs)) # self.__httpd.allow_reuse_address = True self.__thread = Thread(target=self.__httpd.serve_forever) self.__thread.start() @@ -58,7 +76,8 @@ def stop(self): regex = r'bughog_(.+)=(.+)' if self.__httpd: self.__httpd.shutdown() - self.__thread.join() + if self.__thread: + self.__thread.join() self.__httpd.server_close() request_urls = [request['url'] for request in self.data['requests'] if 'url' in request] data = self._parse_bughog_variables(request_urls, regex) diff --git a/bci/web/blueprints/experiments.py b/bci/web/blueprints/experiments.py index 96297cc1..5ef7af53 100644 --- a/bci/web/blueprints/experiments.py +++ b/bci/web/blueprints/experiments.py @@ -39,17 +39,10 @@ def index(): @exp.route("/report/", methods=["GET", "POST"]) def report(): - leak = request.args.get("leak") - if leak is not None: - resp = make_response( - render_template("cookies.html", title="Report", to_report=leak) - ) - else: - resp = make_response( - render_template( - "cookies.html", title="Report", to_report="Nothing to report" - ) - ) + get_params = [item for item in get_all_GET_parameters(request).items()] + resp = make_response( + render_template("cookies.html", title="Report", get_params=get_params) + ) cookie_exp_date = datetime.datetime.now() + datetime.timedelta(weeks=4) resp.set_cookie("generic", "1", expires=cookie_exp_date) @@ -86,7 +79,7 @@ def report_leak_if_using_http(target_scheme): Triggers request to /report/ if a request was received over the specified `scheme`. """ used_scheme = request.headers.get("X-Forwarded-Proto") - params = get_all_bughog_GET_parameters(request) + params = get_all_GET_parameters(request) if used_scheme == target_scheme: return "Redirect", 307, {"Location": url_for("experiments.report", **params)} else: @@ -101,7 +94,7 @@ def report_leak_if_present(expected_header_name: str): if expected_header_name not in request.headers: return f"Header {expected_header_name} not found", 200, {"Allow-CSP-From": "*"} - params = get_all_bughog_GET_parameters(request) + params = get_all_GET_parameters(request) return ( "Redirect", 307, @@ -126,7 +119,7 @@ def report_leak_if_contains(expected_header_name: str, expected_header_value: st {"Allow-CSP-From": "*"}, ) - params = get_all_bughog_GET_parameters(request) + params = get_all_GET_parameters(request) return ( "Redirect", 307, @@ -137,5 +130,5 @@ def report_leak_if_contains(expected_header_name: str, expected_header_value: st ) -def get_all_bughog_GET_parameters(request): - return {k: v for k, v in request.args.items() if k.startswith("bughog_")} +def get_all_GET_parameters(request): + return {k: v for k, v in request.args.items()} diff --git a/bci/web/templates/cookies.html b/bci/web/templates/cookies.html index 320a3907..2e1b0b0c 100644 --- a/bci/web/templates/cookies.html +++ b/bci/web/templates/cookies.html @@ -2,10 +2,15 @@ {% block content %} -{% if to_report %} +{% if get_params %}

Reported:

- -

{{ to_report }}

+ +{% else %} +No GET parameters. {% endif %}

Cookies

@@ -20,4 +25,4 @@

Cookies

elem.appendChild(cookie); } -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/nginx/config/experiments.conf b/nginx/config/experiments.conf index 92c6447c..940b5b9f 100644 --- a/nginx/config/experiments.conf +++ b/nginx/config/experiments.conf @@ -1,6 +1,11 @@ access_log /logs/nginx-access-poc.log default_format; location /res/ { + include /etc/nginx/config/notify_collector.conf; + error_page 504 @resources; +} + +location @resources { root /www/data; } @@ -48,6 +53,11 @@ location ~ ^/(.+)/(.+)/(.+)/$ { } location ~ ^/(.+)/(.+)/(.+)$ { + include /etc/nginx/config/notify_collector.conf; + error_page 504 @experiment; +} + +location @experiment { rewrite ^/(.+)$ /$1/; } diff --git a/nginx/config/notify_collector.conf b/nginx/config/notify_collector.conf new file mode 100644 index 00000000..78bd8fc0 --- /dev/null +++ b/nginx/config/notify_collector.conf @@ -0,0 +1,30 @@ +# We want to notify worker-specific request collectors of every request to our experiment server. +# Hacky solution: pretend that the collector is not reachable and use fallback to serve experiment page. +# So: +# 1. NGINX will send request to request collector of worker. +# 2. NGINX will serve the experiment page. + +proxy_pass http://$remote_addr:5001/report/; +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-Proto $scheme; + +proxy_method POST; +proxy_set_header Content-Type "application/json"; + +set $request_body_data ''; +if ($request_body) { + set $request_body_data "$request_body"; +} + +set $url '"url": "${scheme}://${host}${request_uri}"'; +set $method '"method": "$request_method"'; +set $content '"content": "${request_body_data}"'; +set $report '{${url}, ${method}, ${content}}'; +proxy_set_body $report; + +# We don't need any response, we merely want to notify +proxy_connect_timeout 2s; +proxy_send_timeout 2s; +proxy_read_timeout 0s; # Force a 504 by setting timeout to 0 + From bd0e23bb2ca71ab8a43cb689ffa9e822364d5359 Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Tue, 29 Oct 2024 14:45:44 +0100 Subject: [PATCH 02/57] Custom .py experiment files (#34) * Fix code highlighting modes and add new .py filetype * Add empty testing Python server experiment * Fix Python ace mode * Fix loading Python files * Simple Python server fetch experiment * Basic python directory proxying * Dynamically import and execute Python experiment files * Pass request data to custom Python files * Default Python server template content * Add JSON highlighting * Remove python server headers * Disable creating __pycache__ folder with dynamic imports --- bci/evaluations/custom/custom_evaluation.py | 46 +++++++++++++------ bci/web/blueprints/experiments.py | 22 +++++++++ bci/web/page_parser.py | 4 ++ bci/web/vue/src/components/poc-editor.vue | 18 ++++++-- .../PythonServer/a.test/main/headers.json | 6 +++ .../PythonServer/a.test/main/index.html | 11 +++++ .../PythonServer/a.test/py-server/index.py | 15 ++++++ nginx/config/experiments.conf | 8 ++++ 8 files changed, 112 insertions(+), 18 deletions(-) create mode 100644 experiments/pages/Support/PythonServer/a.test/main/headers.json create mode 100644 experiments/pages/Support/PythonServer/a.test/main/index.html create mode 100644 experiments/pages/Support/PythonServer/a.test/py-server/index.py diff --git a/bci/evaluations/custom/custom_evaluation.py b/bci/evaluations/custom/custom_evaluation.py index b359c036..279065e3 100644 --- a/bci/evaluations/custom/custom_evaluation.py +++ b/bci/evaluations/custom/custom_evaluation.py @@ -166,27 +166,43 @@ def add_page(self, project: str, poc: str, domain: str, path: str, file_type: st if os.path.exists(file_path): return False with open(file_path, 'w') as file: - file.write('') - headers_file_path = os.path.join(page_path, 'headers.json') - if not os.path.exists(headers_file_path): - with open(headers_file_path, 'w') as file: - file.write( - textwrap.dedent( - """\ - [ - { - "key": "Header-Name", - "value": "Header-Value" - } - ] - """ + file.write(self.get_default_file_content(file_type)) + + if self.include_file_headers(file_type): + headers_file_path = os.path.join(page_path, 'headers.json') + if not os.path.exists(headers_file_path): + with open(headers_file_path, 'w') as file: + file.write( + textwrap.dedent( + """\ + [ + { + "key": "Header-Name", + "value": "Header-Value" + } + ] + """ + ) ) - ) self.sync_with_folders() # Notify clients of change (an experiment might now be runnable) Clients.push_experiments_to_all() return True + @staticmethod + def get_default_file_content(file_type: str) -> str: + if file_type != 'py': + return '' + + with open('experiments/pages/Support/PythonServer/a.test/py-server/index.py', 'r') as file: + template_content = file.read() + + return template_content + + @staticmethod + def include_file_headers(file_type: str) -> bool: + return file_type != 'py' + def sync_with_folders(self): self.dir_tree = self.initialize_dir_tree() self.tests_per_project = self.initialize_tests_and_url_queues(self.dir_tree) diff --git a/bci/web/blueprints/experiments.py b/bci/web/blueprints/experiments.py index 5ef7af53..2de8daeb 100644 --- a/bci/web/blueprints/experiments.py +++ b/bci/web/blueprints/experiments.py @@ -1,6 +1,8 @@ import datetime import logging import threading +import importlib.util +import sys import requests from flask import Blueprint, make_response, render_template, request, url_for @@ -130,5 +132,25 @@ def report_leak_if_contains(expected_header_name: str, expected_header_value: st ) +@exp.route("////") +def python_evaluation(project: str, experiment: str, directory: str): + """ + Evaluates the python script and returns its result. + """ + host = request.host.lower() + + module_name = f"{host}/{project}/{experiment}/{directory}" + path = f"experiments/pages/{project}/{experiment}/{host}/{directory}/index.py" + + # Dynamically import the file + sys.dont_write_bytecode = True + spec = importlib.util.spec_from_file_location(module_name, path) + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + spec.loader.exec_module(module) + + return module.main(request) + + def get_all_GET_parameters(request): return {k: v for k, v in request.args.items()} diff --git a/bci/web/page_parser.py b/bci/web/page_parser.py index 1cff781d..57dc6186 100644 --- a/bci/web/page_parser.py +++ b/bci/web/page_parser.py @@ -68,6 +68,10 @@ def get_content(subdir_folder_path: str): { "file_name": "index.js", "content_type": "text/javascript" + }, + { + "file_name": "index.py", + "content_type": "text/x-python" } ] content = None diff --git a/bci/web/vue/src/components/poc-editor.vue b/bci/web/vue/src/components/poc-editor.vue index 7bac6b8e..59e894fb 100644 --- a/bci/web/vue/src/components/poc-editor.vue +++ b/bci/web/vue/src/components/poc-editor.vue @@ -1,5 +1,9 @@ + + \ No newline at end of file diff --git a/experiments/pages/Support/PythonServer/a.test/py-server/index.py b/experiments/pages/Support/PythonServer/a.test/py-server/index.py new file mode 100644 index 00000000..91e823b8 --- /dev/null +++ b/experiments/pages/Support/PythonServer/a.test/py-server/index.py @@ -0,0 +1,15 @@ +from flask import Request + +# Make sure that your page directory starts with 'py-' + +def main(req: Request): + # TODO - implement your functionality and return a Flask response + + return { + "agent": req.headers.get("User-Agent"), + "cookies": req.cookies, + "host": req.host, + "path": req.path, + "scheme": req.scheme, + "url": req.url + } \ No newline at end of file diff --git a/nginx/config/experiments.conf b/nginx/config/experiments.conf index 940b5b9f..398dac9c 100644 --- a/nginx/config/experiments.conf +++ b/nginx/config/experiments.conf @@ -21,6 +21,14 @@ location ~ ^/report(/.+)*/?$ { proxy_set_header X-Forwarded-Proto $scheme; } +location ~ ^/(.+)/(.+)/py-(.+)/$ { + proxy_pass http://core:5000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; +} + location ~ ^/(.+)/(.+)/(.+)/$ { root /www/data/pages; index index.html index.js; From 1da8973323608181205163a1ca06409558a02b3a Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Wed, 30 Oct 2024 12:22:54 +0000 Subject: [PATCH 03/57] WiP - interaction script --- bci/browser/interaction/__init__.py | 0 bci/browser/interaction/browsers/__init__.py | 0 bci/browser/interaction/browsers/browser.py | 82 +++++++++++++++++++ bci/browser/interaction/browsers/chromium.py | 51 ++++++++++++ bci/browser/interaction/browsers/firefox.py | 74 +++++++++++++++++ bci/browser/interaction/interaction.py | 13 +++ bci/evaluations/custom/custom_evaluation.py | 51 ++++++++---- .../UserInteraction/a.test/main/headers.json | 1 + .../UserInteraction/a.test/main/index.html | 6 ++ .../UserInteraction/interaction_script.cmd | 1 + requirements.in | 1 + requirements.txt | 4 +- 12 files changed, 269 insertions(+), 15 deletions(-) create mode 100644 bci/browser/interaction/__init__.py create mode 100644 bci/browser/interaction/browsers/__init__.py create mode 100644 bci/browser/interaction/browsers/browser.py create mode 100644 bci/browser/interaction/browsers/chromium.py create mode 100644 bci/browser/interaction/browsers/firefox.py create mode 100644 bci/browser/interaction/interaction.py create mode 100644 experiments/pages/Support/UserInteraction/a.test/main/headers.json create mode 100644 experiments/pages/Support/UserInteraction/a.test/main/index.html create mode 100644 experiments/pages/Support/UserInteraction/interaction_script.cmd diff --git a/bci/browser/interaction/__init__.py b/bci/browser/interaction/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bci/browser/interaction/browsers/__init__.py b/bci/browser/interaction/browsers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bci/browser/interaction/browsers/browser.py b/bci/browser/interaction/browsers/browser.py new file mode 100644 index 00000000..a969fcce --- /dev/null +++ b/bci/browser/interaction/browsers/browser.py @@ -0,0 +1,82 @@ +import json +from abc import ABC, abstractmethod + +from websockets.sync.client import ClientConnection, connect + + +# Returns true if: +# - required == True -> all fields from `pattern` are present in `data` with the same value +# - required == False -> all fields from `pattern` which are present in `data` have the same value +def dictionaries_match(pattern: dict, data: dict, required: bool) -> bool: + for key in pattern: + if required and key not in data: + return False + + if key in data: + # Equal values, up to slashes + if ( + not dictionaries_match(pattern[key], data[key], required) + if isinstance(pattern[key], dict) + else str(data[key]).replace('/', '') != str(pattern[key]).replace('/', '') + ): + return False + return True + + +class Browser(ABC): + request_id: int = 0 + ws_timeout: float + ws: ClientConnection + + def __init__(self, browser_id: str = '', port: int = 9222, host: str = '127.0.0.1', autoclose_timeout: float = 0.5): + self.ws_timeout = autoclose_timeout + self.ws = connect(self.get_ws_endpoint(host, port, browser_id), close_timeout=autoclose_timeout) + self.initialize_connection(browser_id, port, host) + + def req_id(self) -> int: + self.request_id += 1 + return self.request_id + + def send(self, data: dict) -> dict: + data['id'] = self.req_id() + + self.ws.send(json.dumps(data)) + + return self.receive({'method': data['method'], 'id': data['id']}, False) + + def listen(self, event: str, params: dict) -> dict: + return self.receive({'type': 'event', 'method': event, 'params': params}, True) + + def receive(self, data: dict, required: bool) -> dict: + result = None + + while result == None or (not dictionaries_match(data, result, required)): + result = json.loads(self.ws.recv(self.ws_timeout)) + + if 'type' in result and result['type'] == 'error': + raise Exception(f'Received browser error: {result}') + + return result + + # --- BROWSER-SPECIFIC METHODS --- + @abstractmethod + def get_ws_endpoint(self, host: str, port: int, browser_id: str) -> str: + pass + + @abstractmethod + def initialize_connection(self, _browserId, _port, _host): + pass + + @abstractmethod + def close_connection( + self, + ): + pass + + @abstractmethod + def navigate(self, _url): + pass + + @abstractmethod + def click(self, _x, _y): + pass diff --git a/bci/browser/interaction/browsers/chromium.py b/bci/browser/interaction/browsers/chromium.py new file mode 100644 index 00000000..b82c7014 --- /dev/null +++ b/bci/browser/interaction/browsers/chromium.py @@ -0,0 +1,51 @@ +from time import sleep + +from .browser import Browser + + +class Chromium(Browser): + session_id: str + + def get_ws_endpoint(self, host: str, port: int, browser_id: str) -> str: + return f'ws://{host}:{port}/devtools/browser/{browser_id}' + + def initialize_connection(self, browserId, port, host): + # Get list of all targets and find a "page" target. + target_response = self.send( + { + 'method': 'Target.getTargets', + } + ) + + page_target = list(filter(lambda info: (info['type'] == 'page'), target_response['result']['targetInfos']))[0][ + 'targetId' + ] + + # Attach to the page target. + session = self.send({'method': 'Target.attachToTarget', 'params': {'targetId': page_target, 'flatten': True}}) + + self.session_id = session['result']['sessionId'] + + def close_connection(self): + pass + + def navigate(self, url): + self.send({'sessionId': self.session_id, 'method': 'Page.navigate', 'params': {'url': url}}) + sleep(0.01) + + def click(self, x, y): + self.send( + { + 'sessionId': self.session_id, + 'method': 'Input.dispatchMouseEvent', + 'params': {'x': x, 'y': y, 'type': 'mousePressed', 'clickCount': 1, 'button': 'left'}, + } + ) + + self.send( + { + 'sessionId': self.session_id, + 'method': 'Input.dispatchMouseEvent', + 'params': {'x': x, 'y': y, 'type': 'mouseReleased', 'button': 'left'}, + } + ) diff --git a/bci/browser/interaction/browsers/firefox.py b/bci/browser/interaction/browsers/firefox.py new file mode 100644 index 00000000..97dd7202 --- /dev/null +++ b/bci/browser/interaction/browsers/firefox.py @@ -0,0 +1,74 @@ +from .browser import Browser + + +class Firefox(Browser): + browsing_context: str + + def get_ws_endpoint(self, host: str, port: int, browser_id: str) -> str: + return f'ws://{host}:{port}/session' + + def initialize_connection(self, browserId, port, host): + # Initiate the session + session_id = self.send({'method': 'session.new', 'params': {'capabilities': {}}})['result']['sessionId'] + + # Subscribe to browser events + self.send( + { + 'method': 'session.subscribe', + 'params': { + 'events': [ + 'browsingContext.domContentLoaded', + ] + }, + } + ) + + # Create the browsing context + user_context = self.send({'method': 'browser.createUserContext', 'params': {}})['result']['userContext'] + + self.browsing_context = self.send( + {'method': 'browsingContext.create', 'params': {'type': 'tab', 'userContext': user_context}} + )['result']['context'] + + def close_connection(self): + self.send( + { + 'method': 'session.end', + 'params': {}, + } + ) + + def navigate(self, url): + navigation = self.send( + {'method': 'browsingContext.navigate', 'params': {'url': url, 'context': self.browsing_context}} + )['result']['navigation'] + + # Wait for the DOM to load + self.listen( + 'browsingContext.domContentLoaded', + { + 'url': url, + 'context': self.browsing_context, + 'navigation': navigation, + }, + ) + + def click(self, x, y): + self.send( + { + 'method': 'input.performActions', + 'params': { + 'context': self.browsing_context, + 'actions': [ + { + 'type': 'pointer', + 'id': str(self.req_id()), + 'actions': [ + {'type': 'pointerDown', 'button': 1, 'width': x, 'height': y}, + {'type': 'pointerUp', 'button': 1, 'width': x, 'height': y}, + ], + } + ], + }, + } + ) diff --git a/bci/browser/interaction/interaction.py b/bci/browser/interaction/interaction.py new file mode 100644 index 00000000..8a014ac1 --- /dev/null +++ b/bci/browser/interaction/interaction.py @@ -0,0 +1,13 @@ +from bci.browser.configuration.browser import Browser as BrowserConfig + + +class Interaction: + browser_config: BrowserConfig + script: list[str] + + def __init__(self, browser: BrowserConfig, script: list[str]) -> None: + self.browser_config = browser + self.script = script + + def execute(self) -> None: + print(f'TODO - execute {self.browser_config._get_terminal_args()} with script {", ".join(self.script)}') diff --git a/bci/evaluations/custom/custom_evaluation.py b/bci/evaluations/custom/custom_evaluation.py index 279065e3..3f235378 100644 --- a/bci/evaluations/custom/custom_evaluation.py +++ b/bci/evaluations/custom/custom_evaluation.py @@ -3,6 +3,7 @@ import textwrap from bci.browser.configuration.browser import Browser +from bci.browser.interaction.interaction import Interaction from bci.configuration import Global from bci.evaluations.collectors.collector import Collector, Type from bci.evaluations.evaluation_framework import EvaluationFramework @@ -16,7 +17,7 @@ class CustomEvaluationFramework(EvaluationFramework): def __init__(self): super().__init__() self.dir_tree = self.initialize_dir_tree() - self.tests_per_project = self.initialize_tests_and_url_queues(self.dir_tree) + self.tests_per_project = self.initialize_tests_and_interactions(self.dir_tree) @staticmethod def initialize_dir_tree() -> dict: @@ -35,7 +36,7 @@ def path_to_dict(path): return path_to_dict(path) @staticmethod - def initialize_tests_and_url_queues(dir_tree: dict) -> dict: + def initialize_tests_and_interactions(dir_tree: dict) -> dict: experiments_per_project = {} page_folder_path = Global.custom_page_folder for project, experiments in dir_tree.items(): @@ -43,13 +44,29 @@ def initialize_tests_and_url_queues(dir_tree: dict) -> dict: project_path = os.path.join(page_folder_path, project) experiments_per_project[project] = {} for experiment in experiments: - url_queue = CustomEvaluationFramework.__get_url_queue(project, project_path, experiment) - experiments_per_project[project][experiment] = { - 'url_queue': url_queue, - 'runnable': CustomEvaluationFramework.is_runnable_experiment(project, experiment, dir_tree), - } + interaction_script = CustomEvaluationFramework.__get_interaction_script(project_path, experiment) + data = {} + + if interaction_script is not None: + data['interaction_script'] = interaction_script + else: + url_queue = CustomEvaluationFramework.__get_url_queue(project, project_path, experiment) + data['url_queue'] = url_queue + + data['runnable'] = CustomEvaluationFramework.is_runnable_experiment(project, experiment, dir_tree) + + experiments_per_project[project][experiment] = data return experiments_per_project + @staticmethod + def __get_interaction_script(project_path: str, experiment: str) -> list[str] | None: + interaction_file_path = os.path.join(project_path, experiment, 'interaction_script.cmd') + if os.path.isfile(interaction_file_path): + # If an interaction script is specified, it is parsed and used + with open(interaction_file_path) as file: + return file.readlines() + return None + @staticmethod def __get_url_queue(project: str, project_path: str, experiment: str) -> list[str]: url_queue_file_path = os.path.join(project_path, experiment, 'url_queue.txt') @@ -88,12 +105,18 @@ def perform_specific_evaluation(self, browser: Browser, params: TestParameters) is_dirty = False try: - url_queue = self.tests_per_project[params.evaluation_configuration.project][params.mech_group]['url_queue'] - for url in url_queue: - tries = 0 - while tries < 3: - tries += 1 - browser.visit(url) + experiment = self.tests_per_project[params.evaluation_configuration.project][params.mech_group] + + if 'interaction_script' in experiment: + interaction = Interaction(browser, experiment['interaction_script']) + interaction.execute() + else: + url_queue = experiment['url_queue'] + for url in url_queue: + tries = 0 + while tries < 3: + tries += 1 + browser.visit(url) except Exception as e: logger.error(f'Error during test: {e}', exc_info=True) is_dirty = True @@ -205,5 +228,5 @@ def include_file_headers(file_type: str) -> bool: def sync_with_folders(self): self.dir_tree = self.initialize_dir_tree() - self.tests_per_project = self.initialize_tests_and_url_queues(self.dir_tree) + self.tests_per_project = self.initialize_tests_and_interactions(self.dir_tree) logger.info('Framework is synced with folders') diff --git a/experiments/pages/Support/UserInteraction/a.test/main/headers.json b/experiments/pages/Support/UserInteraction/a.test/main/headers.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/experiments/pages/Support/UserInteraction/a.test/main/headers.json @@ -0,0 +1 @@ +[] diff --git a/experiments/pages/Support/UserInteraction/a.test/main/index.html b/experiments/pages/Support/UserInteraction/a.test/main/index.html new file mode 100644 index 00000000..ad482c63 --- /dev/null +++ b/experiments/pages/Support/UserInteraction/a.test/main/index.html @@ -0,0 +1,6 @@ + + CLICK + + \ No newline at end of file diff --git a/experiments/pages/Support/UserInteraction/interaction_script.cmd b/experiments/pages/Support/UserInteraction/interaction_script.cmd new file mode 100644 index 00000000..5daa56cf --- /dev/null +++ b/experiments/pages/Support/UserInteraction/interaction_script.cmd @@ -0,0 +1 @@ +NAVIGATE https://a.test/Support/UserInteraction/main diff --git a/requirements.in b/requirements.in index b7055ad5..385a39ca 100644 --- a/requirements.in +++ b/requirements.in @@ -5,3 +5,4 @@ flatten-dict gunicorn pymongo requests +websockets \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index d2968cc8..a5f13430 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.11 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # pip-compile requirements.in @@ -54,6 +54,8 @@ urllib3==2.2.3 # via # docker # requests +websockets==13.1 + # via -r requirements.in werkzeug==3.0.4 # via flask wsproto==1.2.0 From 5a516ba2765d2dc8bab09293163390ce1c3f8e93 Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Wed, 30 Oct 2024 14:10:09 +0000 Subject: [PATCH 04/57] Connect browser interaction to terminal automation --- bci/browser/automation/terminal.py | 35 +++++++++++++-------- bci/browser/configuration/browser.py | 21 ++++++++++--- bci/browser/configuration/chromium.py | 2 ++ bci/browser/interaction/interaction.py | 17 ++++++++-- bci/evaluations/custom/custom_evaluation.py | 8 +++-- 5 files changed, 61 insertions(+), 22 deletions(-) diff --git a/bci/browser/automation/terminal.py b/bci/browser/automation/terminal.py index d1fe6f70..ed2235e1 100644 --- a/bci/browser/automation/terminal.py +++ b/bci/browser/automation/terminal.py @@ -7,22 +7,31 @@ class TerminalAutomation: - @staticmethod - def run(url: str, args: list[str], seconds_per_visit: int): - logger.debug("Starting browser process...") + def visit_url(url: str, args: list[str], seconds_per_visit: int): args.append(url) + proc, _ = TerminalAutomation.open_browser(args) + logger.debug(f'Visiting the page for {seconds_per_visit}s') + time.sleep(seconds_per_visit) + TerminalAutomation.terminate_browser(proc, args) + + @staticmethod + def open_browser(args: list[str]) -> tuple[subprocess.Popen, str]: + logger.debug('Starting browser process...') logger.debug(f'Command string: \'{" ".join(args)}\'') - with open('/tmp/browser.log', 'a') as file: - proc = subprocess.Popen( - args, - stdout=file, - stderr=file - ) + with open('/tmp/browser.log', 'a+') as file: + proc = subprocess.Popen(args, stdout=file, stderr=file) + time.sleep(0.5) + position = file.tell() + file.seek(0) + output = file.read() + file.seek(position) + return proc, output - time.sleep(seconds_per_visit) + @staticmethod + def terminate_browser(proc: subprocess.Popen, args: list[str]) -> None: + logger.debug('Terminating browser process using SIGINT...') - logger.debug(f'Terminating browser process after {seconds_per_visit}s using SIGINT...') # Use SIGINT and SIGTERM to end process such that cookies remain saved. proc.send_signal(signal.SIGINT) proc.send_signal(signal.SIGTERM) @@ -30,8 +39,8 @@ def run(url: str, args: list[str], seconds_per_visit: int): try: stdout, stderr = proc.communicate(timeout=5) except subprocess.TimeoutExpired: - logger.info("Browser process did not terminate after 5s. Killing process through pkill...") + logger.info('Browser process did not terminate after 5s. Killing process through pkill...') subprocess.run(['pkill', '-2', args[0].split('/')[-1]]) proc.wait() - logger.debug("Browser process terminated.") + logger.debug('Browser process terminated.') diff --git a/bci/browser/configuration/browser.py b/bci/browser/configuration/browser.py index 1178ece1..67419cd4 100644 --- a/bci/browser/configuration/browser.py +++ b/bci/browser/configuration/browser.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +import subprocess from abc import abstractmethod import bci.browser.binary.factory as binary_factory @@ -15,8 +16,11 @@ class Browser: + process: subprocess.Popen - def __init__(self, browser_config: BrowserConfiguration, eval_config: EvaluationConfiguration, binary: Binary) -> None: + def __init__( + self, browser_config: BrowserConfiguration, eval_config: EvaluationConfiguration, binary: Binary + ) -> None: self.browser_config = browser_config self.eval_config = eval_config self.binary = binary @@ -34,10 +38,17 @@ def visit(self, url: str): match self.eval_config.automation: case 'terminal': args = self._get_terminal_args() - TerminalAutomation.run(url, args, self.eval_config.seconds_per_visit) + TerminalAutomation.visit_url(url, args, self.eval_config.seconds_per_visit) case _: raise AttributeError('Not implemented') + def open(self) -> str: + self.process, output = TerminalAutomation.open_browser(self._get_terminal_args()) + return output + + def terminate(self): + TerminalAutomation.terminate_browser(self.process, self._get_terminal_args()) + def pre_evaluation_setup(self): self.__fetch_binary() @@ -80,11 +91,13 @@ def _get_executable_file_path(self) -> str: return os.path.join(self.__get_execution_folder_path(), self.binary.executable_name) @abstractmethod - def _get_terminal_args(self): + def _get_terminal_args(self) -> list[str]: pass @staticmethod - def get_browser(browser_config: BrowserConfiguration, eval_config: EvaluationConfiguration, state: State) -> Browser: + def get_browser( + browser_config: BrowserConfiguration, eval_config: EvaluationConfiguration, state: State + ) -> Browser: from bci.browser.configuration.chromium import Chromium from bci.browser.configuration.firefox import Firefox diff --git a/bci/browser/configuration/chromium.py b/bci/browser/configuration/chromium.py index 6108328d..3aa0f8e3 100644 --- a/bci/browser/configuration/chromium.py +++ b/bci/browser/configuration/chromium.py @@ -1,6 +1,7 @@ from bci.browser.configuration.browser import Browser from bci.browser.configuration.options import Default, BlockThirdPartyCookies, PrivateBrowsing from bci.browser.configuration.profile import prepare_chromium_profile +from bci.browser.interaction.interaction import Interaction SUPPORTED_OPTIONS = [ Default(), @@ -41,6 +42,7 @@ def _get_terminal_args(self) -> list[str]: args.append('--enable-logging') args.append('--v=1') args.append('--log-level=0') + args.append(f'--remote-debugging-port={Interaction.port}') # Headless changed from version +/- 110 onwards: https://developer.chrome.com/docs/chromium/new-headless # Using the `--headless` flag will crash the browser for these later versions. # Also see: https://github.com/DistriNet/BugHog/issues/12 diff --git a/bci/browser/interaction/interaction.py b/bci/browser/interaction/interaction.py index 8a014ac1..1f867a0a 100644 --- a/bci/browser/interaction/interaction.py +++ b/bci/browser/interaction/interaction.py @@ -1,13 +1,24 @@ +from bci.browser.automation.terminal import TerminalAutomation from bci.browser.configuration.browser import Browser as BrowserConfig class Interaction: - browser_config: BrowserConfig + port = 9222 + + browser: BrowserConfig script: list[str] def __init__(self, browser: BrowserConfig, script: list[str]) -> None: - self.browser_config = browser + self.browser = browser self.script = script def execute(self) -> None: - print(f'TODO - execute {self.browser_config._get_terminal_args()} with script {", ".join(self.script)}') + output = self.browser.open() + + # TODO - identify browser from the output + # TODO - initialize the browser + # TODO - parse the script and run the commands + # TODO - visit the sanity check URL + # TODO - destroy the browser + + self.browser.terminate() diff --git a/bci/evaluations/custom/custom_evaluation.py b/bci/evaluations/custom/custom_evaluation.py index 3f235378..b2d97ef3 100644 --- a/bci/evaluations/custom/custom_evaluation.py +++ b/bci/evaluations/custom/custom_evaluation.py @@ -107,14 +107,18 @@ def perform_specific_evaluation(self, browser: Browser, params: TestParameters) try: experiment = self.tests_per_project[params.evaluation_configuration.project][params.mech_group] + max_tries = 3 if 'interaction_script' in experiment: interaction = Interaction(browser, experiment['interaction_script']) - interaction.execute() + tries = 0 + while tries < max_tries: + tries += 1 + interaction.execute() else: url_queue = experiment['url_queue'] for url in url_queue: tries = 0 - while tries < 3: + while tries < max_tries: tries += 1 browser.visit(url) except Exception as e: From 6cef7597894fef52a5236a8e5521440453b855ff Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Wed, 30 Oct 2024 14:26:05 +0000 Subject: [PATCH 05/57] Install pip requirements in dev container --- .devcontainer/devcontainer.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 8687890c..16cc9c75 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -24,7 +24,10 @@ "Vue.volar" ] } - } + }, + + // Install pip requirements + "postCreateCommand": "pip install -r requirements.txt" // Features to add to the dev container. More info: https://containers.dev/features. // "features": {}, @@ -35,9 +38,6 @@ // Uncomment the next line if you want to keep your containers running after VS Code shuts down. // "shutdownAction": "none", - // Uncomment the next line to run commands after the container is created. - // "postCreateCommand": "cat /etc/os-release", - // Configure tool-specific properties. // "customizations": {}, } From 688948fcb7748dd5011c355b1c6e716043a0f56d Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Wed, 30 Oct 2024 14:43:36 +0000 Subject: [PATCH 06/57] Simple interaction browser identification --- bci/browser/interaction/browsers/chromium.py | 2 +- bci/browser/interaction/interaction.py | 25 +++++++++++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/bci/browser/interaction/browsers/chromium.py b/bci/browser/interaction/browsers/chromium.py index b82c7014..7db878af 100644 --- a/bci/browser/interaction/browsers/chromium.py +++ b/bci/browser/interaction/browsers/chromium.py @@ -31,7 +31,7 @@ def close_connection(self): def navigate(self, url): self.send({'sessionId': self.session_id, 'method': 'Page.navigate', 'params': {'url': url}}) - sleep(0.01) + sleep(0.5) def click(self, x, y): self.send( diff --git a/bci/browser/interaction/interaction.py b/bci/browser/interaction/interaction.py index 1f867a0a..518d5ee4 100644 --- a/bci/browser/interaction/interaction.py +++ b/bci/browser/interaction/interaction.py @@ -1,5 +1,8 @@ -from bci.browser.automation.terminal import TerminalAutomation +import re + from bci.browser.configuration.browser import Browser as BrowserConfig +from bci.browser.interaction.browsers.browser import Browser +from bci.browser.interaction.browsers.chromium import Chromium class Interaction: @@ -15,10 +18,20 @@ def __init__(self, browser: BrowserConfig, script: list[str]) -> None: def execute(self) -> None: output = self.browser.open() - # TODO - identify browser from the output - # TODO - initialize the browser - # TODO - parse the script and run the commands - # TODO - visit the sanity check URL - # TODO - destroy the browser + interaction_browser = self._initiate_browser(output) + + # TODO - parse the script and run the commands instead + interaction_browser.navigate('https://a.test/Support/UserInteraction/main') + + interaction_browser.navigate('https://a.test/report/?bughog_sanity_check=OK') + interaction_browser.close_connection() self.browser.terminate() + + def _initiate_browser(self, init_output: str) -> Browser: + cdp = re.search(r'DevTools listening on ws:\/\/127\.0\.0\.1:9222\/devtools\/browser\/(.+)\n', init_output) + + if cdp: + return Chromium(browser_id=cdp.group(1), port=Interaction.port) + + raise Exception('Unrecognized browser') From b2f1f828e1bbb3b9615b659787f10f8e7bd35661 Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Wed, 30 Oct 2024 14:48:26 +0000 Subject: [PATCH 07/57] Fix browser log reading --- bci/browser/automation/terminal.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bci/browser/automation/terminal.py b/bci/browser/automation/terminal.py index ed2235e1..77869206 100644 --- a/bci/browser/automation/terminal.py +++ b/bci/browser/automation/terminal.py @@ -20,12 +20,13 @@ def open_browser(args: list[str]) -> tuple[subprocess.Popen, str]: logger.debug('Starting browser process...') logger.debug(f'Command string: \'{" ".join(args)}\'') with open('/tmp/browser.log', 'a+') as file: + initial_position = file.tell() proc = subprocess.Popen(args, stdout=file, stderr=file) time.sleep(0.5) - position = file.tell() - file.seek(0) + last_position = file.tell() + file.seek(initial_position) output = file.read() - file.seek(position) + file.seek(last_position) return proc, output @staticmethod From e8f56a2903c5868ae76ce12ed82b28476ed91743 Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Wed, 30 Oct 2024 15:19:17 +0000 Subject: [PATCH 08/57] Browser interaction commands interpreter --- bci/browser/interaction/browsers/browser.py | 2 ++ bci/browser/interaction/interaction.py | 28 +++++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/bci/browser/interaction/browsers/browser.py b/bci/browser/interaction/browsers/browser.py index a969fcce..fcee439b 100644 --- a/bci/browser/interaction/browsers/browser.py +++ b/bci/browser/interaction/browsers/browser.py @@ -28,6 +28,8 @@ class Browser(ABC): ws_timeout: float ws: ClientConnection + public_methods: list[str] = ['navigate', 'click'] + def __init__(self, browser_id: str = '', port: int = 9222, host: str = '127.0.0.1', autoclose_timeout: float = 0.5): self.ws_timeout = autoclose_timeout self.ws = connect(self.get_ws_endpoint(host, port, browser_id), close_timeout=autoclose_timeout) diff --git a/bci/browser/interaction/interaction.py b/bci/browser/interaction/interaction.py index 518d5ee4..65b582b0 100644 --- a/bci/browser/interaction/interaction.py +++ b/bci/browser/interaction/interaction.py @@ -1,9 +1,13 @@ +import logging import re +from inspect import signature from bci.browser.configuration.browser import Browser as BrowserConfig from bci.browser.interaction.browsers.browser import Browser from bci.browser.interaction.browsers.chromium import Chromium +logger = logging.getLogger(__name__) + class Interaction: port = 9222 @@ -20,8 +24,7 @@ def execute(self) -> None: interaction_browser = self._initiate_browser(output) - # TODO - parse the script and run the commands instead - interaction_browser.navigate('https://a.test/Support/UserInteraction/main') + self._interpret(interaction_browser) interaction_browser.navigate('https://a.test/report/?bughog_sanity_check=OK') interaction_browser.close_connection() @@ -35,3 +38,24 @@ def _initiate_browser(self, init_output: str) -> Browser: return Chromium(browser_id=cdp.group(1), port=Interaction.port) raise Exception('Unrecognized browser') + + def _interpret(self, browser: Browser) -> None: + for statement in self.script: + cmd, *args = statement.split() + method_name = cmd.lower() + + if method_name not in Browser.public_methods: + raise Exception( + f'Invalid command `{cmd}`. Expected one of {", ".join(map(lambda m: m.upper(), Browser.public_methods))}.' + ) + + method = getattr(browser, method_name) + method_params_len = len(signature(method).parameters) + + if method_params_len != len(args): + raise Exception( + f'Invalid number of arguments for command `{cmd}`. Expected {method_params_len}, got {len(args)}.' + ) + + logger.debug(f'Executing interaction method `{method_name}` with the arguments {args}') + method(*args) From c0e47903e97a42eded3617864d2166275d7af134 Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Wed, 30 Oct 2024 16:55:44 +0000 Subject: [PATCH 09/57] Basic BiDi implementation --- bci/browser/configuration/chromium.py | 3 +-- bci/browser/configuration/firefox.py | 4 ++-- bci/browser/interaction/browsers/browser.py | 2 +- bci/browser/interaction/browsers/firefox.py | 11 ++++++----- bci/browser/interaction/interaction.py | 20 ++++++++++++++++---- 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/bci/browser/configuration/chromium.py b/bci/browser/configuration/chromium.py index 3aa0f8e3..c2d70ea4 100644 --- a/bci/browser/configuration/chromium.py +++ b/bci/browser/configuration/chromium.py @@ -1,7 +1,6 @@ from bci.browser.configuration.browser import Browser from bci.browser.configuration.options import Default, BlockThirdPartyCookies, PrivateBrowsing from bci.browser.configuration.profile import prepare_chromium_profile -from bci.browser.interaction.interaction import Interaction SUPPORTED_OPTIONS = [ Default(), @@ -42,7 +41,7 @@ def _get_terminal_args(self) -> list[str]: args.append('--enable-logging') args.append('--v=1') args.append('--log-level=0') - args.append(f'--remote-debugging-port={Interaction.port}') + args.append('--remote-debugging-port=0') # Headless changed from version +/- 110 onwards: https://developer.chrome.com/docs/chromium/new-headless # Using the `--headless` flag will crash the browser for these later versions. # Also see: https://github.com/DistriNet/BugHog/issues/12 diff --git a/bci/browser/configuration/firefox.py b/bci/browser/configuration/firefox.py index 643ae57c..c6918d70 100644 --- a/bci/browser/configuration/firefox.py +++ b/bci/browser/configuration/firefox.py @@ -2,10 +2,9 @@ from bci import cli from bci.browser.configuration.browser import Browser -from bci.browser.configuration.options import Default, BlockThirdPartyCookies, PrivateBrowsing, TrackingProtection +from bci.browser.configuration.options import BlockThirdPartyCookies, Default, PrivateBrowsing, TrackingProtection from bci.browser.configuration.profile import prepare_firefox_profile - SUPPORTED_OPTIONS = [ Default(), BlockThirdPartyCookies(), @@ -26,6 +25,7 @@ def _get_terminal_args(self) -> list[str]: args = [self._get_executable_file_path()] args.extend(['-profile', self._profile_path]) + args.append('--remote-debugging-port=0') user_prefs = [] def add_user_pref(key: str, value: str | int | bool): diff --git a/bci/browser/interaction/browsers/browser.py b/bci/browser/interaction/browsers/browser.py index fcee439b..94dcc5ff 100644 --- a/bci/browser/interaction/browsers/browser.py +++ b/bci/browser/interaction/browsers/browser.py @@ -30,7 +30,7 @@ class Browser(ABC): public_methods: list[str] = ['navigate', 'click'] - def __init__(self, browser_id: str = '', port: int = 9222, host: str = '127.0.0.1', autoclose_timeout: float = 0.5): + def __init__(self, browser_id: str = '', port: int = 9222, host: str = '127.0.0.1', autoclose_timeout: float = 2): self.ws_timeout = autoclose_timeout self.ws = connect(self.get_ws_endpoint(host, port, browser_id), close_timeout=autoclose_timeout) self.initialize_connection(browser_id, port, host) diff --git a/bci/browser/interaction/browsers/firefox.py b/bci/browser/interaction/browsers/firefox.py index 97dd7202..9f5d6f02 100644 --- a/bci/browser/interaction/browsers/firefox.py +++ b/bci/browser/interaction/browsers/firefox.py @@ -46,11 +46,12 @@ def navigate(self, url): # Wait for the DOM to load self.listen( 'browsingContext.domContentLoaded', - { - 'url': url, - 'context': self.browsing_context, - 'navigation': navigation, - }, + {} + #{ + # 'url': url, + # 'context': self.browsing_context, + # 'navigation': navigation, + #}, ) def click(self, x, y): diff --git a/bci/browser/interaction/interaction.py b/bci/browser/interaction/interaction.py index 65b582b0..a3c98488 100644 --- a/bci/browser/interaction/interaction.py +++ b/bci/browser/interaction/interaction.py @@ -5,13 +5,12 @@ from bci.browser.configuration.browser import Browser as BrowserConfig from bci.browser.interaction.browsers.browser import Browser from bci.browser.interaction.browsers.chromium import Chromium +from bci.browser.interaction.browsers.firefox import Firefox logger = logging.getLogger(__name__) class Interaction: - port = 9222 - browser: BrowserConfig script: list[str] @@ -32,10 +31,23 @@ def execute(self) -> None: self.browser.terminate() def _initiate_browser(self, init_output: str) -> Browser: - cdp = re.search(r'DevTools listening on ws:\/\/127\.0\.0\.1:9222\/devtools\/browser\/(.+)\n', init_output) + print(init_output) + + cdp = re.search( + r'DevTools listening on ws:\/\/(.+):(.+)\/devtools\/browser\/(.+)\n', + init_output, + ) if cdp: - return Chromium(browser_id=cdp.group(1), port=Interaction.port) + return Chromium(browser_id=cdp.group(3), port=int(cdp.group(2)), host=cdp.group(1)) + + bidi = re.search( + r'WebDriver BiDi listening on ws:\/\/(.+):(.+)', + init_output, + ) + + if bidi: + return Firefox(port=int(bidi.group(2)), host=bidi.group(1)) raise Exception('Unrecognized browser') From 796bf93e22007beaeeae4b518270f19f6e418a22 Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Wed, 30 Oct 2024 16:57:12 +0000 Subject: [PATCH 10/57] Remove print --- bci/browser/interaction/interaction.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bci/browser/interaction/interaction.py b/bci/browser/interaction/interaction.py index a3c98488..f0186382 100644 --- a/bci/browser/interaction/interaction.py +++ b/bci/browser/interaction/interaction.py @@ -31,8 +31,6 @@ def execute(self) -> None: self.browser.terminate() def _initiate_browser(self, init_output: str) -> Browser: - print(init_output) - cdp = re.search( r'DevTools listening on ws:\/\/(.+):(.+)\/devtools\/browser\/(.+)\n', init_output, From c2c5580561c09294f6082897c3984f74a66c9e6e Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Thu, 31 Oct 2024 08:55:56 +0000 Subject: [PATCH 11/57] Simple AutoGUI test experiment --- experiments/pages/Support/AutoGUI/a.test/main/headers.json | 1 + experiments/pages/Support/AutoGUI/a.test/main/index.html | 1 + experiments/pages/Support/AutoGUI/interaction_script.cmd | 2 ++ 3 files changed, 4 insertions(+) create mode 100644 experiments/pages/Support/AutoGUI/a.test/main/headers.json create mode 100644 experiments/pages/Support/AutoGUI/a.test/main/index.html create mode 100644 experiments/pages/Support/AutoGUI/interaction_script.cmd diff --git a/experiments/pages/Support/AutoGUI/a.test/main/headers.json b/experiments/pages/Support/AutoGUI/a.test/main/headers.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/experiments/pages/Support/AutoGUI/a.test/main/headers.json @@ -0,0 +1 @@ +[] diff --git a/experiments/pages/Support/AutoGUI/a.test/main/index.html b/experiments/pages/Support/AutoGUI/a.test/main/index.html new file mode 100644 index 00000000..4c9318e8 --- /dev/null +++ b/experiments/pages/Support/AutoGUI/a.test/main/index.html @@ -0,0 +1 @@ +CLICK \ No newline at end of file diff --git a/experiments/pages/Support/AutoGUI/interaction_script.cmd b/experiments/pages/Support/AutoGUI/interaction_script.cmd new file mode 100644 index 00000000..92690174 --- /dev/null +++ b/experiments/pages/Support/AutoGUI/interaction_script.cmd @@ -0,0 +1,2 @@ +NAVIGATE https://a.test/Support/AutoGUI/main +CLICK 100 100 \ No newline at end of file From b8c02dd5be326749c7cadd03504887dd61f28181 Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Thu, 31 Oct 2024 09:01:13 +0000 Subject: [PATCH 12/57] Install AutoGUI --- requirements.in | 3 ++- requirements.txt | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/requirements.in b/requirements.in index 385a39ca..814ea5bf 100644 --- a/requirements.in +++ b/requirements.in @@ -5,4 +5,5 @@ flatten-dict gunicorn pymongo requests -websockets \ No newline at end of file +websockets +pyautogui \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index a5f13430..c03e9c00 100644 --- a/requirements.txt +++ b/requirements.txt @@ -38,10 +38,30 @@ markupsafe==3.0.2 # via # jinja2 # werkzeug +mouseinfo==0.1.3 + # via pyautogui packaging==24.1 # via gunicorn +pyautogui==0.9.54 + # via -r requirements.in +pygetwindow==0.0.9 + # via pyautogui pymongo==4.10.1 # via -r requirements.in +pymsgbox==1.0.9 + # via pyautogui +pyperclip==1.9.0 + # via mouseinfo +pyrect==0.2.0 + # via pygetwindow +pyscreeze==1.0.1 + # via pyautogui +python3-xlib==0.15 + # via + # mouseinfo + # pyautogui +pytweening==1.2.0 + # via pyautogui requests==2.32.3 # via # -r requirements.in From 1444ff2de22fbede606e9c9f3ea64b00c924c1b0 Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Thu, 31 Oct 2024 10:07:16 +0000 Subject: [PATCH 13/57] Simple AutoGUI usage --- Dockerfile | 4 + bci/browser/configuration/browser.py | 13 ++- bci/browser/interaction/browsers/browser.py | 106 +++++++------------- bci/browser/interaction/interaction.py | 13 ++- requirements.in | 6 +- requirements.txt | 12 ++- 6 files changed, 67 insertions(+), 87 deletions(-) diff --git a/Dockerfile b/Dockerfile index b7bd1020..9439b3d8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -54,7 +54,11 @@ RUN cp /app/scripts/daemon/xvfb /etc/init.d/xvfb # Install python packages COPY requirements.txt /app/requirements.txt RUN pip install --user -r /app/requirements.txt +RUN apt-get install python3-tk python3-xlib gnome-screenshot -y +# Initiate PyAutoGUI +RUN touch /root/.Xauthority && \ + xauth add ${HOST}:0 . $(xxd -l 16 -p /dev/urandom) FROM base AS core # Copy rest of source code diff --git a/bci/browser/configuration/browser.py b/bci/browser/configuration/browser.py index 67419cd4..707f53d3 100644 --- a/bci/browser/configuration/browser.py +++ b/bci/browser/configuration/browser.py @@ -16,12 +16,13 @@ class Browser: - process: subprocess.Popen + process: subprocess.Popen | None def __init__( self, browser_config: BrowserConfiguration, eval_config: EvaluationConfiguration, binary: Binary ) -> None: self.browser_config = browser_config + self.process = None self.eval_config = eval_config self.binary = binary self.state = binary.state @@ -42,12 +43,18 @@ def visit(self, url: str): case _: raise AttributeError('Not implemented') - def open(self) -> str: - self.process, output = TerminalAutomation.open_browser(self._get_terminal_args()) + def open(self, url: str) -> str: + args = self._get_terminal_args() + args.append(url) + self.process, output = TerminalAutomation.open_browser(args) return output def terminate(self): + if self.process is None: + return + TerminalAutomation.terminate_browser(self.process, self._get_terminal_args()) + self.process = None def pre_evaluation_setup(self): self.__fetch_binary() diff --git a/bci/browser/interaction/browsers/browser.py b/bci/browser/interaction/browsers/browser.py index 94dcc5ff..cdfec4ae 100644 --- a/bci/browser/interaction/browsers/browser.py +++ b/bci/browser/interaction/browsers/browser.py @@ -1,84 +1,46 @@ -import json -from abc import ABC, abstractmethod +import base64 +import os +from io import BytesIO +from time import sleep -from websockets.sync.client import ClientConnection, connect +import pyautogui as gui +import Xlib.display +from pyvirtualdisplay.display import Display +from bci.browser.configuration.browser import Browser as BrowserConfig -# Returns true if: -# - required == True -> all fields from `pattern` are present in `data` with the same value -# - required == False -> all fields from `pattern` which are present in `data` have the same value -def dictionaries_match(pattern: dict, data: dict, required: bool) -> bool: - for key in pattern: - if required and key not in data: - return False - - if key in data: - # Equal values, up to slashes - if ( - not dictionaries_match(pattern[key], data[key], required) - if isinstance(pattern[key], dict) - else str(data[key]).replace('/', '') != str(pattern[key]).replace('/', '') - ): - return False - return True - - -class Browser(ABC): - request_id: int = 0 - ws_timeout: float - ws: ClientConnection +class Browser: + browser_config: BrowserConfig public_methods: list[str] = ['navigate', 'click'] - def __init__(self, browser_id: str = '', port: int = 9222, host: str = '127.0.0.1', autoclose_timeout: float = 2): - self.ws_timeout = autoclose_timeout - self.ws = connect(self.get_ws_endpoint(host, port, browser_id), close_timeout=autoclose_timeout) - self.initialize_connection(browser_id, port, host) - - def req_id(self) -> int: - self.request_id += 1 - return self.request_id - - def send(self, data: dict) -> dict: - data['id'] = self.req_id() - - self.ws.send(json.dumps(data)) - - return self.receive({'method': data['method'], 'id': data['id']}, False) - - def listen(self, event: str, params: dict) -> dict: - return self.receive({'type': 'event', 'method': event, 'params': params}, True) - - def receive(self, data: dict, required: bool) -> dict: - result = None - - while result == None or (not dictionaries_match(data, result, required)): - result = json.loads(self.ws.recv(self.ws_timeout)) - - if 'type' in result and result['type'] == 'error': - raise Exception(f'Received browser error: {result}') + def __init__(self, browser_config: BrowserConfig): + self.browser_config = browser_config + disp = Display(visible=True, size=(1920, 1080), backend='xvfb', use_xauth=True) + disp.start() + gui._pyautogui_x11._display = Xlib.display.Display(os.environ['DISPLAY']) - return result + def __del__(self): + self.browser_config.terminate() - # --- BROWSER-SPECIFIC METHODS --- - @abstractmethod - def get_ws_endpoint(self, host: str, port: int, browser_id: str) -> str: - pass + # --- PUBLIC METHODS --- + def navigate(self, url: str): + self.browser_config.terminate() + self.browser_config.open(url) - @abstractmethod - def initialize_connection(self, _browserId, _port, _host): - pass + # TODO - convert this into an argument or a separate command + sleep(0.5) - @abstractmethod - def close_connection( - self, - ): - pass + def click(self, x: str, y: str): + # print(gui.size()) + # print(gui.position()) + # gui.moveTo(int(x), int(y)) + gui.moveTo(100, 540) + gui.click() - @abstractmethod - def navigate(self, _url): - pass + # buffered = BytesIO() + # print(gui.screenshot().save(buffered, format='JPEG')) + # img_str = base64.b64encode(buffered.getvalue()) + # print(img_str.decode('utf-8')) - @abstractmethod - def click(self, _x, _y): - pass + sleep(0.5) diff --git a/bci/browser/interaction/interaction.py b/bci/browser/interaction/interaction.py index f0186382..e84c4328 100644 --- a/bci/browser/interaction/interaction.py +++ b/bci/browser/interaction/interaction.py @@ -19,18 +19,16 @@ def __init__(self, browser: BrowserConfig, script: list[str]) -> None: self.script = script def execute(self) -> None: - output = self.browser.open() - - interaction_browser = self._initiate_browser(output) + interaction_browser = self._initiate_browser() self._interpret(interaction_browser) interaction_browser.navigate('https://a.test/report/?bughog_sanity_check=OK') - interaction_browser.close_connection() - - self.browser.terminate() - def _initiate_browser(self, init_output: str) -> Browser: + def _initiate_browser(self) -> Browser: + # TODO - possibly return different browser instances + return Browser(self.browser) + """ cdp = re.search( r'DevTools listening on ws:\/\/(.+):(.+)\/devtools\/browser\/(.+)\n', init_output, @@ -48,6 +46,7 @@ def _initiate_browser(self, init_output: str) -> Browser: return Firefox(port=int(bidi.group(2)), host=bidi.group(1)) raise Exception('Unrecognized browser') + """ def _interpret(self, browser: Browser) -> None: for statement in self.script: diff --git a/requirements.in b/requirements.in index 814ea5bf..f0993275 100644 --- a/requirements.in +++ b/requirements.in @@ -5,5 +5,7 @@ flatten-dict gunicorn pymongo requests -websockets -pyautogui \ No newline at end of file +pyautogui +pyvirtualdisplay +Pillow +Xlib \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c03e9c00..2a367b96 100644 --- a/requirements.txt +++ b/requirements.txt @@ -42,6 +42,8 @@ mouseinfo==0.1.3 # via pyautogui packaging==24.1 # via gunicorn +pillow==11.0.0 + # via -r requirements.in pyautogui==0.9.54 # via -r requirements.in pygetwindow==0.0.9 @@ -62,6 +64,8 @@ python3-xlib==0.15 # pyautogui pytweening==1.2.0 # via pyautogui +pyvirtualdisplay==3.0 + # via -r requirements.in requests==2.32.3 # via # -r requirements.in @@ -69,14 +73,16 @@ requests==2.32.3 simple-websocket==1.1.0 # via flask-sock six==1.16.0 - # via flatten-dict + # via + # flatten-dict + # xlib urllib3==2.2.3 # via # docker # requests -websockets==13.1 - # via -r requirements.in werkzeug==3.0.4 # via flask wsproto==1.2.0 # via simple-websocket +xlib==0.21 + # via -r requirements.in From 0acc9c204d9cba8d7862167f7139b9a63be516a4 Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Thu, 31 Oct 2024 10:17:02 +0000 Subject: [PATCH 14/57] Remove debugging ports --- bci/browser/configuration/chromium.py | 1 - bci/browser/configuration/firefox.py | 1 - 2 files changed, 2 deletions(-) diff --git a/bci/browser/configuration/chromium.py b/bci/browser/configuration/chromium.py index c2d70ea4..6108328d 100644 --- a/bci/browser/configuration/chromium.py +++ b/bci/browser/configuration/chromium.py @@ -41,7 +41,6 @@ def _get_terminal_args(self) -> list[str]: args.append('--enable-logging') args.append('--v=1') args.append('--log-level=0') - args.append('--remote-debugging-port=0') # Headless changed from version +/- 110 onwards: https://developer.chrome.com/docs/chromium/new-headless # Using the `--headless` flag will crash the browser for these later versions. # Also see: https://github.com/DistriNet/BugHog/issues/12 diff --git a/bci/browser/configuration/firefox.py b/bci/browser/configuration/firefox.py index c6918d70..c336190b 100644 --- a/bci/browser/configuration/firefox.py +++ b/bci/browser/configuration/firefox.py @@ -25,7 +25,6 @@ def _get_terminal_args(self) -> list[str]: args = [self._get_executable_file_path()] args.extend(['-profile', self._profile_path]) - args.append('--remote-debugging-port=0') user_prefs = [] def add_user_pref(key: str, value: str | int | bool): From 840190a8cdd5447039e4c732e32548cae0ffb83d Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Thu, 31 Oct 2024 11:14:46 +0000 Subject: [PATCH 15/57] Fix opening Firefox --- bci/browser/automation/terminal.py | 12 +++--------- bci/browser/configuration/browser.py | 5 ++--- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/bci/browser/automation/terminal.py b/bci/browser/automation/terminal.py index 77869206..ddc08b8e 100644 --- a/bci/browser/automation/terminal.py +++ b/bci/browser/automation/terminal.py @@ -10,24 +10,18 @@ class TerminalAutomation: @staticmethod def visit_url(url: str, args: list[str], seconds_per_visit: int): args.append(url) - proc, _ = TerminalAutomation.open_browser(args) + proc = TerminalAutomation.open_browser(args) logger.debug(f'Visiting the page for {seconds_per_visit}s') time.sleep(seconds_per_visit) TerminalAutomation.terminate_browser(proc, args) @staticmethod - def open_browser(args: list[str]) -> tuple[subprocess.Popen, str]: + def open_browser(args: list[str]) -> subprocess.Popen: logger.debug('Starting browser process...') logger.debug(f'Command string: \'{" ".join(args)}\'') with open('/tmp/browser.log', 'a+') as file: - initial_position = file.tell() proc = subprocess.Popen(args, stdout=file, stderr=file) - time.sleep(0.5) - last_position = file.tell() - file.seek(initial_position) - output = file.read() - file.seek(last_position) - return proc, output + return proc @staticmethod def terminate_browser(proc: subprocess.Popen, args: list[str]) -> None: diff --git a/bci/browser/configuration/browser.py b/bci/browser/configuration/browser.py index 707f53d3..0247f28e 100644 --- a/bci/browser/configuration/browser.py +++ b/bci/browser/configuration/browser.py @@ -43,11 +43,10 @@ def visit(self, url: str): case _: raise AttributeError('Not implemented') - def open(self, url: str) -> str: + def open(self, url: str) -> None: args = self._get_terminal_args() args.append(url) - self.process, output = TerminalAutomation.open_browser(args) - return output + self.process = TerminalAutomation.open_browser(args) def terminate(self): if self.process is None: From c85fa2a198af6a45748fbad00606ed692da4e59d Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Thu, 31 Oct 2024 11:15:15 +0000 Subject: [PATCH 16/57] Fix opening Firefox --- bci/browser/configuration/firefox.py | 2 ++ bci/browser/interaction/browsers/browser.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/bci/browser/configuration/firefox.py b/bci/browser/configuration/firefox.py index c336190b..4310bf20 100644 --- a/bci/browser/configuration/firefox.py +++ b/bci/browser/configuration/firefox.py @@ -25,6 +25,7 @@ def _get_terminal_args(self) -> list[str]: args = [self._get_executable_file_path()] args.extend(['-profile', self._profile_path]) + args.append('-setDefaultBrowser') user_prefs = [] def add_user_pref(key: str, value: str | int | bool): @@ -44,6 +45,7 @@ def add_user_pref(key: str, value: str | int | bool): # add_user_pref('network.proxy.type', 1) add_user_pref('app.update.enabled', False) + add_user_pref('browser.shell.checkDefaultBrowser', False) if 'default' in self.browser_config.browser_setting: pass elif 'btpc' in self.browser_config.browser_setting: diff --git a/bci/browser/interaction/browsers/browser.py b/bci/browser/interaction/browsers/browser.py index cdfec4ae..2d45dafd 100644 --- a/bci/browser/interaction/browsers/browser.py +++ b/bci/browser/interaction/browsers/browser.py @@ -29,12 +29,13 @@ def navigate(self, url: str): self.browser_config.open(url) # TODO - convert this into an argument or a separate command - sleep(0.5) + sleep(2) def click(self, x: str, y: str): # print(gui.size()) # print(gui.position()) # gui.moveTo(int(x), int(y)) + gui.moveTo(100, 540) gui.click() From e7cfbd94e5320788f89cf277a4dc882636db75bb Mon Sep 17 00:00:00 2001 From: Gertjan Date: Thu, 31 Oct 2024 11:18:28 +0000 Subject: [PATCH 17/57] Improve PoC editor - Change internal tree representation for experiments - Store python backend logic as files without folder - Existing url_queue.txt files can be editor through web UI --- bci/evaluations/custom/custom_evaluation.py | 41 ++++++--- bci/web/blueprints/api.py | 78 +++++++++++----- bci/web/blueprints/experiments.py | 68 +++++++------- bci/web/vue/src/components/poc-editor.vue | 90 ++++++++++++++----- .../a.test/{py-server/index.py => server.py} | 2 +- nginx/config/experiments.conf | 51 +++++------ 6 files changed, 215 insertions(+), 115 deletions(-) rename experiments/pages/Support/PythonServer/a.test/{py-server/index.py => server.py} (98%) diff --git a/bci/evaluations/custom/custom_evaluation.py b/bci/evaluations/custom/custom_evaluation.py index 279065e3..66130646 100644 --- a/bci/evaluations/custom/custom_evaluation.py +++ b/bci/evaluations/custom/custom_evaluation.py @@ -20,19 +20,29 @@ def __init__(self): @staticmethod def initialize_dir_tree() -> dict: + """ + Initializes directory tree of experiments. + """ path = Global.custom_page_folder - - def path_to_dict(path): - if os.path.isdir(path): - return { - sub_folder: path_to_dict(os.path.join(path, sub_folder)) - for sub_folder in os.listdir(path) - if sub_folder != 'url_queue.txt' - } + dir_tree = {} + + def set_nested_value(d: dict, keys: list[str], value: dict): + nested_dict = d + for key in keys[:-1]: + nested_dict = nested_dict[key] + nested_dict[keys[-1]] = value + + for root, dirs, files in os.walk(path): + # Remove base path from root + root = root[len(path):] + keys = root.split('/')[1:] + subdir_tree = {dir: {} for dir in dirs} | {file: None for file in files} + if root: + set_nested_value(dir_tree, keys, subdir_tree) else: - return os.path.basename(path) + dir_tree = subdir_tree - return path_to_dict(path) + return dir_tree @staticmethod def initialize_tests_and_url_queues(dir_tree: dict) -> dict: @@ -70,11 +80,14 @@ def __get_url_queue(project: str, project_path: str, experiment: str) -> list[st raise AttributeError(f"Could not infer url queue for experiment '{experiment}' in project '{project}'") @staticmethod - def is_runnable_experiment(project: str, poc: str, dir_tree: dict) -> bool: + def is_runnable_experiment(project: str, poc: str, dir_tree: dict[str,dict]) -> bool: domains = dir_tree[project][poc] - if not (poc_main_path := [paths for domain, paths in domains.items() if 'main' in paths]): + # Should have exactly one main folder + main_paths = [paths for paths in domains.values() if paths is not None and 'main' in paths.keys()] + if len(main_paths) != 1: return False - if 'index.html' not in poc_main_path[0]['main'].keys(): + # Main should have index.html + if 'index.html' not in main_paths[0]['main'].keys(): return False return True @@ -194,7 +207,7 @@ def get_default_file_content(file_type: str) -> str: if file_type != 'py': return '' - with open('experiments/pages/Support/PythonServer/a.test/py-server/index.py', 'r') as file: + with open('experiments/pages/Support/PythonServer/a.test/server.py', 'r') as file: template_content = file.read() return template_content diff --git a/bci/web/blueprints/api.py b/bci/web/blueprints/api.py index 592fbcab..1828c526 100644 --- a/bci/web/blueprints/api.py +++ b/bci/web/blueprints/api.py @@ -16,8 +16,10 @@ THREAD = None -def start_thread(func, args=None) -> bool: +def __start_thread(func, args=None) -> bool: global THREAD + if args is None: + args = [] if THREAD and THREAD.is_alive(): return False else: @@ -26,7 +28,6 @@ def start_thread(func, args=None) -> bool: return True - @api.before_request def check_readiness(): if not bci_api.is_ready(): @@ -55,9 +56,15 @@ def add_headers(response): @api.route('/evaluation/start/', methods=['POST']) def start_evaluation(): + if request.json is None: + return { + 'status': 'NOK', + 'msg': "No evaluation parameters found" + } + data = request.json.copy() params = evaluation_factory(data) - if start_thread(bci_api.run, args=[params]): + if __start_thread(bci_api.run, args=[params]): return { 'status': 'OK' } @@ -69,6 +76,12 @@ def start_evaluation(): @api.route('/evaluation/stop/', methods=['POST']) def stop_evaluation(): + if request.json is None: + return { + 'status': 'NOK', + 'msg': "No stop parameters found" + } + data = request.json.copy() forcefully = data.get('forcefully', False) if forcefully: @@ -140,6 +153,12 @@ def log(): @api.route('/data/', methods=['PUT']) def data_source(): + if request.json is None: + return { + 'status': 'NOK', + 'msg': "No data parameters found" + } + params = request.json.copy() revision_data, version_data = bci_api.get_data_sources(params) if revision_data or version_data: @@ -172,30 +191,41 @@ def poc(project: str, poc: str): } -@api.route('/poc//////', methods=['GET']) -def poc_file(project: str, poc: str, domain: str, path: str, file: str): - return { - 'status': 'OK', - 'content': bci_api.get_poc_file(project, poc, domain, path, file) - } - - -@api.route('/poc//////', methods=['POST']) -def update_poc_file(project: str, poc: str, domain: str, path: str, file: str): - data = request.json.copy() - success = bci_api.update_poc_file(project, poc, domain, path, file, data['content']) - if success: +@api.route('/poc////', methods=['GET', 'POST']) +def get_poc_file_content(project: str, poc: str, file: str): + domain = request.args.get('domain', '') + path = request.args.get('path', '') + if request.method == 'GET': return { - 'status': 'OK' - } - else : - return { - 'status': 'NOK' + 'status': 'OK', + 'content': bci_api.get_poc_file(project, poc, domain, path, file) } + else: + if not request.json: + return { + 'status': 'NOK', + 'msg': 'No content to update file with' + } + data = request.json.copy() + success = bci_api.update_poc_file(project, poc, domain, path, file, data['content']) + if success: + return { + 'status': 'OK' + } + else : + return { + 'status': 'NOK' + } @api.route('/poc///', methods=['POST']) def add_page(project: str, poc: str): + if request.json is None: + return { + 'status': 'NOK', + 'msg': "No page parameters found" + } + data = request.json.copy() success = bci_api.add_page(project, poc, data['domain'], data['page'], data['file_type']) if success: @@ -218,6 +248,12 @@ def get_available_domains(): @api.route('/poc//', methods=['POST']) def create_experiment(project: str): + if request.json is None: + return { + 'status': 'NOK', + 'msg': "No experiment parameters found" + } + data = request.json.copy() if 'poc_name' not in data.keys(): return { diff --git a/bci/web/blueprints/experiments.py b/bci/web/blueprints/experiments.py index 2de8daeb..b5cb5945 100644 --- a/bci/web/blueprints/experiments.py +++ b/bci/web/blueprints/experiments.py @@ -5,7 +5,7 @@ import sys import requests -from flask import Blueprint, make_response, render_template, request, url_for +from flask import Blueprint, Request, make_response, render_template, request, url_for from bci.web.page_parser import load_experiment_pages @@ -26,6 +26,7 @@ @exp.before_request def before_request(): + __report(request) host = request.host.lower() if host not in ALLOWED_DOMAINS: logger.error( @@ -34,29 +35,13 @@ def before_request(): return f"Host '{host}' is not supported by this framework." -@exp.route("/") -def index(): - return f"This page is visited over {request.scheme}." - - -@exp.route("/report/", methods=["GET", "POST"]) -def report(): - get_params = [item for item in get_all_GET_parameters(request).items()] - resp = make_response( - render_template("cookies.html", title="Report", get_params=get_params) - ) - - cookie_exp_date = datetime.datetime.now() + datetime.timedelta(weeks=4) - resp.set_cookie("generic", "1", expires=cookie_exp_date) - resp.set_cookie("secure", "1", expires=cookie_exp_date, secure=True) - resp.set_cookie("httpOnly", "1", expires=cookie_exp_date, httponly=True) - resp.set_cookie("lax", "1", expires=cookie_exp_date, samesite="lax") - resp.set_cookie("strict", "1", expires=cookie_exp_date, samesite="strict") - +def __report(request: Request) -> None: + """ + Submit report to BugHog + """ # Respond to collector on same IP # remote_ip = request.remote_addr remote_ip = request.headers.get("X-Real-IP") - response_data = { "url": request.url, "method": request.method, @@ -72,6 +57,29 @@ def send_report_to_collector(): threading.Thread(target=send_report_to_collector).start() + +def __get_all_GET_parameters(request) -> dict[str,str]: + return {k: v for k, v in request.args.items()} + + +@exp.route("/") +def index(): + return f"This page is visited over {request.scheme}." + + +@exp.route("/report/", methods=["GET", "POST"]) +def report_endpoint(): + get_params = [item for item in __get_all_GET_parameters(request).items()] + resp = make_response( + render_template("cookies.html", title="Report", get_params=get_params) + ) + + cookie_exp_date = datetime.datetime.now() + datetime.timedelta(weeks=4) + resp.set_cookie("generic", "1", expires=cookie_exp_date) + resp.set_cookie("secure", "1", expires=cookie_exp_date, secure=True) + resp.set_cookie("httpOnly", "1", expires=cookie_exp_date, httponly=True) + resp.set_cookie("lax", "1", expires=cookie_exp_date, samesite="lax") + resp.set_cookie("strict", "1", expires=cookie_exp_date, samesite="strict") return resp @@ -81,7 +89,7 @@ def report_leak_if_using_http(target_scheme): Triggers request to /report/ if a request was received over the specified `scheme`. """ used_scheme = request.headers.get("X-Forwarded-Proto") - params = get_all_GET_parameters(request) + params = __get_all_GET_parameters(request) if used_scheme == target_scheme: return "Redirect", 307, {"Location": url_for("experiments.report", **params)} else: @@ -96,7 +104,7 @@ def report_leak_if_present(expected_header_name: str): if expected_header_name not in request.headers: return f"Header {expected_header_name} not found", 200, {"Allow-CSP-From": "*"} - params = get_all_GET_parameters(request) + params = __get_all_GET_parameters(request) return ( "Redirect", 307, @@ -121,7 +129,7 @@ def report_leak_if_contains(expected_header_name: str, expected_header_value: st {"Allow-CSP-From": "*"}, ) - params = get_all_GET_parameters(request) + params = __get_all_GET_parameters(request) return ( "Redirect", 307, @@ -132,15 +140,15 @@ def report_leak_if_contains(expected_header_name: str, expected_header_value: st ) -@exp.route("////") -def python_evaluation(project: str, experiment: str, directory: str): +@exp.route("///.py") +def python_evaluation(project: str, experiment: str, file_name: str): """ Evaluates the python script and returns its result. """ host = request.host.lower() - module_name = f"{host}/{project}/{experiment}/{directory}" - path = f"experiments/pages/{project}/{experiment}/{host}/{directory}/index.py" + module_name = f"{host}/{project}/{experiment}" + path = f"experiments/pages/{project}/{experiment}/{host}/{file_name}.py" # Dynamically import the file sys.dont_write_bytecode = True @@ -150,7 +158,3 @@ def python_evaluation(project: str, experiment: str, directory: str): spec.loader.exec_module(module) return module.main(request) - - -def get_all_GET_parameters(request): - return {k: v for k, v in request.args.items()} diff --git a/bci/web/vue/src/components/poc-editor.vue b/bci/web/vue/src/components/poc-editor.vue index 59e894fb..278e1189 100644 --- a/bci/web/vue/src/components/poc-editor.vue +++ b/bci/web/vue/src/components/poc-editor.vue @@ -66,8 +66,15 @@ this.active_file.name = file_name; const project = this.project; const poc = this.active_poc.name; - const path = `/api/poc/${project}/${poc}/${domain}/${file_path}/${file_name}/`; - axios.get(path) + const params = {}; + if (domain !== null) { + params["domain"] = domain; + } + if (file_path !== null) { + params["path"] = file_path; + } + const path = `/api/poc/${project}/${poc}/${file_name}/`; + axios.get(path, {params: params}) .then((res) => { if (res.data.status === "OK") { this.active_file.should_update_server = false; @@ -101,8 +108,17 @@ const domain = this.active_poc.active_domain; const file_path = this.active_poc.active_path; const file_name = this.active_file.name; - const path = `/api/poc/${project}/${poc}/${domain}/${file_path}/${file_name}/`; - axios.post(path, {"content": this.editor.session.getValue()}) + const params = { + "content": this.editor.session.getValue() + }; + if (domain !== null) { + params["domain"] = domain; + } + if (file_path !== null) { + params["path"] = file_path; + } + const path = `/api/poc/${project}/${poc}/${file_name}/`; + axios.post(path, {"content": this.editor.session.getValue()}, {params: params}) .then((res) => { if (res.data.status == "NOK") { console.error("Could not update file on server"); @@ -115,7 +131,6 @@ }, update_poc_tree(poc_name) { const active_poc_name = poc_name === undefined ? this.active_poc.name : poc_name; - console.log(active_poc_name); const path = `/api/poc/${this.project}/${active_poc_name}/`; axios.get(path) .then((res) => { @@ -199,29 +214,60 @@
+ + + +
+

+

Choose config type:
+ + +

+
+ + +
+
+
diff --git a/bci/web/vue/src/main.js b/bci/web/vue/src/main.js index 70ff98b0..121570e3 100644 --- a/bci/web/vue/src/main.js +++ b/bci/web/vue/src/main.js @@ -5,8 +5,8 @@ import 'flowbite' import 'axios' import { OhVueIcon, addIcons } from "oh-vue-icons"; -import { MdInfooutline, FaRegularEdit, FaLink } from "oh-vue-icons/icons"; +import { MdInfooutline, FaRegularEdit, FaLink, FaPlus } from "oh-vue-icons/icons"; -addIcons(MdInfooutline, FaRegularEdit, FaLink); +addIcons(MdInfooutline, FaRegularEdit, FaLink, FaPlus); const app = createApp(App); app.component("v-icon", OhVueIcon).mount('#app') diff --git a/bci/web/vue/src/style.css b/bci/web/vue/src/style.css index 713de6c3..12c7eb4c 100644 --- a/bci/web/vue/src/style.css +++ b/bci/web/vue/src/style.css @@ -3,7 +3,7 @@ @tailwind utilities; .button { - @apply text-white bg-blue-700 hover:bg-blue-800 active:ring-4 focus:outline-none font-medium rounded-lg text-sm px-4 py-2.5 text-center items-center dark:hover:bg-blue-700 dark:focus:ring-blue-800 + @apply text-white bg-blue-700 hover:bg-blue-800 active:ring-4 focus:outline-none cursor-pointer font-medium rounded-lg text-sm px-4 py-2.5 text-center items-center dark:hover:bg-blue-700 dark:focus:ring-blue-800 } .dropdown-head { @@ -192,7 +192,7 @@ h1 { line-height: 1.1; } -button { +button:not(.no-style) { border-radius: 8px; border: 1px solid transparent; padding: 0.6em 1.2em; @@ -203,8 +203,8 @@ button { cursor: pointer; transition: border-color 0.25s; } -button:focus, -button:focus-visible { +button:not(.no-style):focus, +button:not(.no-style):focus-visible { outline: 4px auto -webkit-focus-ring-color; } From acdec3cf51e5c1e4b5f5e2dfc2993b924767789e Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Fri, 1 Nov 2024 10:42:29 +0000 Subject: [PATCH 32/57] Adding config files through the frontend - Python part --- bci/evaluations/custom/custom_evaluation.py | 16 ++++++++++++++++ bci/main.py | 4 ++++ bci/web/blueprints/api.py | 14 ++++++++++++++ bci/web/vue/src/components/poc-editor.vue | 4 ++-- 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/bci/evaluations/custom/custom_evaluation.py b/bci/evaluations/custom/custom_evaluation.py index 84517c9c..baa464a3 100644 --- a/bci/evaluations/custom/custom_evaluation.py +++ b/bci/evaluations/custom/custom_evaluation.py @@ -207,6 +207,22 @@ def add_page(self, project: str, poc: str, domain: str, path: str, file_type: st # Notify clients of change (an experiment might now be runnable) Clients.push_experiments_to_all() return True + + def add_config(self, project: str, poc: str, type: str) -> bool: + content = self.get_default_file_content(type) + + if (content == ''): + return False + + file_path = os.path.join(Global.custom_page_folder, project, poc, type) + with open(file_path, 'w') as file: + file.write(content) + + self.sync_with_folders() + # Notify clients of change (an experiment might now be runnable) + Clients.push_experiments_to_all() + + return True @staticmethod def get_default_file_content(file_type: str) -> str: diff --git a/bci/main.py b/bci/main.py index 127c1c29..fddb53ae 100644 --- a/bci/main.py +++ b/bci/main.py @@ -155,6 +155,10 @@ def get_available_domains() -> list[str]: @staticmethod def add_page(project: str, poc: str, domain: str, path: str, file_type: str) -> bool: return Main.master.evaluation_framework.add_page(project, poc, domain, path, file_type) + + @staticmethod + def add_config(project: str, poc: str, type: str) -> bool: + return Main.master.evaluation_framework.add_config(project, poc, type) @staticmethod def sigint_handler(signum, frame): diff --git a/bci/web/blueprints/api.py b/bci/web/blueprints/api.py index 592fbcab..8c1094b8 100644 --- a/bci/web/blueprints/api.py +++ b/bci/web/blueprints/api.py @@ -208,6 +208,20 @@ def add_page(project: str, poc: str): } +@api.route('/poc///config', methods=['POST']) +def add_config(project: str, poc: str): + data = request.json.copy() + success = bci_api.add_config(project, poc, data['type']) + if success: + return { + 'status': 'OK' + } + else: + return { + 'status': 'NOK' + } + + @api.route('/poc/domain/', methods=['GET']) def get_available_domains(): return { diff --git a/bci/web/vue/src/components/poc-editor.vue b/bci/web/vue/src/components/poc-editor.vue index 78d2f526..45cffbed 100644 --- a/bci/web/vue/src/components/poc-editor.vue +++ b/bci/web/vue/src/components/poc-editor.vue @@ -47,8 +47,8 @@ 'py', ], available_config_types: { - 'interaction_script': 'Interaction script', - 'url_queue': 'URL queue' + 'interaction_script.cmd': 'Interaction script', + 'url_queue.txt': 'URL queue' }, poc_tree_config: { domain: 'Config', From 3ee4d20e19d748bcd2210b846966e8ea1deb6516 Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Fri, 1 Nov 2024 12:06:47 +0000 Subject: [PATCH 33/57] More interaction commands --- bci/browser/interaction/interaction.py | 14 ++++---- bci/browser/interaction/simulation.py | 35 ++++++++++++++++--- .../default_files/interaction_script.cmd | 17 +++++---- bci/web/vue/src/interaction_script_mode.js | 2 +- .../Support/AutoGUI/a.test/main/index.html | 6 +++- .../Support/AutoGUI/interaction_script.cmd | 19 ++++++++-- experiments/res/bughog.css | 2 +- 7 files changed, 71 insertions(+), 24 deletions(-) diff --git a/bci/browser/interaction/interaction.py b/bci/browser/interaction/interaction.py index 5da2716d..4561fff5 100644 --- a/bci/browser/interaction/interaction.py +++ b/bci/browser/interaction/interaction.py @@ -22,32 +22,30 @@ def execute(self) -> None: simulation = Simulation(self.browser, self.params) self._interpret(simulation) + simulation.sleep('0.5') simulation.navigate('https://a.test/report/?bughog_sanity_check=OK') - simulation.sleep('0.5') def _interpret(self, simulation: Simulation) -> None: for statement in self.script: - if statement.strip() == '': + if statement.strip() == '' or statement[0] == '#': continue cmd, *args = statement.split() method_name = cmd.lower() - if cmd == '#': - continue - if method_name not in Simulation.public_methods: raise Exception( f'Invalid command `{cmd}`. Expected one of {", ".join(map(lambda m: m.upper(), Simulation.public_methods))}.' ) method = getattr(simulation, method_name) - method_params_len = len(signature(method).parameters) + method_params = list(signature(method).parameters.values()) - if method_params_len != len(args): + # Allow different number of arguments only for variable argument number (*) + if len(method_params) != len(args) and (len(method_params) < 1 or str(method_params[0])[0] != '*'): raise Exception( - f'Invalid number of arguments for command `{cmd}`. Expected {method_params_len}, got {len(args)}.' + f'Invalid number of arguments for command `{cmd}`. Expected {len(method_params)}, got {len(args)}.' ) logger.debug(f'Executing interaction method `{method_name}` with the arguments {args}') diff --git a/bci/browser/interaction/simulation.py b/bci/browser/interaction/simulation.py index da22069f..68da367e 100644 --- a/bci/browser/interaction/simulation.py +++ b/bci/browser/interaction/simulation.py @@ -13,7 +13,18 @@ class Simulation: browser_config: BrowserConfig params: TestParameters - public_methods: list[str] = ['navigate', 'click', 'click_el', 'sleep', 'screenshot'] + public_methods: list[str] = [ + 'navigate', + 'click_position', + 'click', + 'write', + 'press', + 'hold', + 'release', + 'hotkey', + 'sleep', + 'screenshot', + ] def __init__(self, browser_config: BrowserConfig, params: TestParameters): self.browser_config = browser_config @@ -37,17 +48,33 @@ def parse_position(self, position: str, max_value: int) -> int: def navigate(self, url: str): self.browser_config.terminate() self.browser_config.open(url) + self.sleep('0.5') - def click(self, x: str, y: str): + def click_position(self, x: str, y: str): max_x, max_y = gui.size() gui.moveTo(self.parse_position(x, max_x), self.parse_position(y, max_y)) gui.click() - def click_el(self, el_id: str): + def click(self, el_id: str): el_image_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), f'elements/{el_id}.png') x, y = gui.locateCenterOnScreen(el_image_path) - self.click(str(x), str(y)) + self.click_position(str(x), str(y)) + + def write(self, text: str): + gui.write(text, interval=0.1) + + def press(self, key: str): + gui.press(key) + + def hold(self, key: str): + gui.keyDown(key) + + def release(self, key: str): + gui.keyUp(key) + + def hotkey(self, *keys: str): + gui.hotkey(*keys) def sleep(self, duration: str): sleep(float(duration)) diff --git a/bci/evaluations/custom/default_files/interaction_script.cmd b/bci/evaluations/custom/default_files/interaction_script.cmd index 59f6d22b..63ad6d9b 100644 --- a/bci/evaluations/custom/default_files/interaction_script.cmd +++ b/bci/evaluations/custom/default_files/interaction_script.cmd @@ -1,7 +1,12 @@ -# TODO - add your interaction script +# TODO - add your interaction script using the commands -# NAVIGATE https://a.test/Project/Experiment/main -# SLEEP 1 -# SCREENSHOT file_name -# CLICK_EL one -# SLEEP 1 \ No newline at end of file +# NAVIGATE url +# CLICK_POSITION x y where x and y are absolute numbers or screen percentages +# CLICK element_id where element_id is one of one, two, three, four, five, six +# WRITE text +# PRESS key +# HOLD key +# RELEASE key +# HOTKEY key1 key2 ... +# SLEEP seconds where seconds is a float or an int +# SCREENSHOT file_name \ No newline at end of file diff --git a/bci/web/vue/src/interaction_script_mode.js b/bci/web/vue/src/interaction_script_mode.js index 35785b44..4413d521 100644 --- a/bci/web/vue/src/interaction_script_mode.js +++ b/bci/web/vue/src/interaction_script_mode.js @@ -1,4 +1,4 @@ -const KEYWORDS = "NAVIGATE|CLICK|CLICK_EL|SLEEP|SCREENSHOT"; +const KEYWORDS = "NAVIGATE|CLICK_POSITION|CLICK|WRITE|PRESS|HOLD|RELEASE|HOTKEY|SLEEP|SCREENSHOT"; ace.define("ace/mode/interaction_script_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"], function(require, exports, module){"use strict"; const oop = require("../lib/oop"); diff --git a/experiments/pages/Support/AutoGUI/a.test/main/index.html b/experiments/pages/Support/AutoGUI/a.test/main/index.html index 70d626e3..e2b6d3c4 100644 --- a/experiments/pages/Support/AutoGUI/a.test/main/index.html +++ b/experiments/pages/Support/AutoGUI/a.test/main/index.html @@ -5,6 +5,10 @@ - CLICK +
+ + + +
\ No newline at end of file diff --git a/experiments/pages/Support/AutoGUI/interaction_script.cmd b/experiments/pages/Support/AutoGUI/interaction_script.cmd index 895572c5..a9ed5d45 100644 --- a/experiments/pages/Support/AutoGUI/interaction_script.cmd +++ b/experiments/pages/Support/AutoGUI/interaction_script.cmd @@ -1,5 +1,18 @@ NAVIGATE https://a.test/Support/AutoGUI/main SLEEP 1 -SCREENSHOT testtest -CLICK_EL three -SLEEP 1 \ No newline at end of file +SCREENSHOT test + +CLICK one +WRITE AutoGUI +HOTKEY ctrl a +HOTKEY ctrl c + +CLICK two + +# Equivalent to HOTKEY ctrl v +HOLD ctrl +HOLD v +RELEASE v +RELEASE ctrl + +PRESS Enter \ No newline at end of file diff --git a/experiments/res/bughog.css b/experiments/res/bughog.css index 7cdbfa7d..fd01d4cd 100644 --- a/experiments/res/bughog.css +++ b/experiments/res/bughog.css @@ -1,7 +1,7 @@ #fullscreen, #one, #two, #three, #four, #five, #six { position: fixed; z-index: 10; - font-size: 0; + font-size: 1px; border: none; outline: none; resize: none; From 52211d03ee147b2f08f5a2f9a1ef7f0aae177fff Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Fri, 1 Nov 2024 12:11:14 +0000 Subject: [PATCH 34/57] Styling fix --- bci/web/vue/src/style.css | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/bci/web/vue/src/style.css b/bci/web/vue/src/style.css index 12c7eb4c..c52649c6 100644 --- a/bci/web/vue/src/style.css +++ b/bci/web/vue/src/style.css @@ -192,7 +192,7 @@ h1 { line-height: 1.1; } -button:not(.no-style) { +button { border-radius: 8px; border: 1px solid transparent; padding: 0.6em 1.2em; @@ -203,11 +203,17 @@ button:not(.no-style) { cursor: pointer; transition: border-color 0.25s; } -button:not(.no-style):focus, -button:not(.no-style):focus-visible { +button:focus, +button:focus-visible { outline: 4px auto -webkit-focus-ring-color; } +button.no-style { + padding: 0; + background: transparent; + border: none; +} + .card { padding: 2em; } From 639441c6c4d7144609a2568ed759751f25d3ccb8 Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Fri, 1 Nov 2024 13:25:22 +0000 Subject: [PATCH 35/57] Final interaction improvements --- bci/browser/configuration/browser.py | 4 ++++ bci/browser/configuration/chromium.py | 3 +++ bci/browser/configuration/firefox.py | 3 +++ bci/browser/interaction/interaction.py | 17 +++++++++++------ bci/browser/interaction/simulation.py | 2 +- .../Support/AutoGUI/a.test/main/index.html | 2 +- .../Support/AutoGUI/interaction_script.cmd | 4 ++-- experiments/res/bughog.css | 14 +++++++------- 8 files changed, 32 insertions(+), 17 deletions(-) diff --git a/bci/browser/configuration/browser.py b/bci/browser/configuration/browser.py index 0247f28e..f6a9f2e7 100644 --- a/bci/browser/configuration/browser.py +++ b/bci/browser/configuration/browser.py @@ -100,6 +100,10 @@ def _get_executable_file_path(self) -> str: def _get_terminal_args(self) -> list[str]: pass + @abstractmethod + def get_navigation_sleep_duration(self) -> int: + pass + @staticmethod def get_browser( browser_config: BrowserConfiguration, eval_config: EvaluationConfiguration, state: State diff --git a/bci/browser/configuration/chromium.py b/bci/browser/configuration/chromium.py index 6108328d..f2cd16cb 100644 --- a/bci/browser/configuration/chromium.py +++ b/bci/browser/configuration/chromium.py @@ -32,6 +32,9 @@ class Chromium(Browser): + def get_navigation_sleep_duration(self) -> int: + return 1 + def _get_terminal_args(self) -> list[str]: assert self._profile_path is not None diff --git a/bci/browser/configuration/firefox.py b/bci/browser/configuration/firefox.py index 4310bf20..cff1d7f1 100644 --- a/bci/browser/configuration/firefox.py +++ b/bci/browser/configuration/firefox.py @@ -20,6 +20,9 @@ class Firefox(Browser): + def get_navigation_sleep_duration(self) -> int: + return 2 + def _get_terminal_args(self) -> list[str]: assert self._profile_path is not None diff --git a/bci/browser/interaction/interaction.py b/bci/browser/interaction/interaction.py index 4561fff5..14fb8d1b 100644 --- a/bci/browser/interaction/interaction.py +++ b/bci/browser/interaction/interaction.py @@ -21,12 +21,11 @@ def __init__(self, browser: BrowserConfig, script: list[str], params: TestParame def execute(self) -> None: simulation = Simulation(self.browser, self.params) - self._interpret(simulation) - simulation.sleep('0.5') + if self._interpret(simulation): + simulation.sleep(str(self.browser.get_navigation_sleep_duration())) + simulation.navigate('https://a.test/report/?bughog_sanity_check=OK') - simulation.navigate('https://a.test/report/?bughog_sanity_check=OK') - - def _interpret(self, simulation: Simulation) -> None: + def _interpret(self, simulation: Simulation) -> bool: for statement in self.script: if statement.strip() == '' or statement[0] == '#': continue @@ -49,4 +48,10 @@ def _interpret(self, simulation: Simulation) -> None: ) logger.debug(f'Executing interaction method `{method_name}` with the arguments {args}') - method(*args) + + try: + method(*args) + except: + return False + + return True diff --git a/bci/browser/interaction/simulation.py b/bci/browser/interaction/simulation.py index 68da367e..d4ba899c 100644 --- a/bci/browser/interaction/simulation.py +++ b/bci/browser/interaction/simulation.py @@ -48,7 +48,7 @@ def parse_position(self, position: str, max_value: int) -> int: def navigate(self, url: str): self.browser_config.terminate() self.browser_config.open(url) - self.sleep('0.5') + self.sleep(str(self.browser_config.get_navigation_sleep_duration())) def click_position(self, x: str, y: str): max_x, max_y = gui.size() diff --git a/experiments/pages/Support/AutoGUI/a.test/main/index.html b/experiments/pages/Support/AutoGUI/a.test/main/index.html index e2b6d3c4..93eb4fc7 100644 --- a/experiments/pages/Support/AutoGUI/a.test/main/index.html +++ b/experiments/pages/Support/AutoGUI/a.test/main/index.html @@ -5,7 +5,7 @@ -
+ diff --git a/experiments/pages/Support/AutoGUI/interaction_script.cmd b/experiments/pages/Support/AutoGUI/interaction_script.cmd index a9ed5d45..91d6f21f 100644 --- a/experiments/pages/Support/AutoGUI/interaction_script.cmd +++ b/experiments/pages/Support/AutoGUI/interaction_script.cmd @@ -1,12 +1,12 @@ NAVIGATE https://a.test/Support/AutoGUI/main -SLEEP 1 -SCREENSHOT test +SCREENSHOT click1 CLICK one WRITE AutoGUI HOTKEY ctrl a HOTKEY ctrl c +SCREENSHOT click2 CLICK two # Equivalent to HOTKEY ctrl v diff --git a/experiments/res/bughog.css b/experiments/res/bughog.css index fd01d4cd..8fe70435 100644 --- a/experiments/res/bughog.css +++ b/experiments/res/bughog.css @@ -30,14 +30,14 @@ #two { background-color: #dcbeff; color: #dcbeff; - top: 45px; + top: 70px; left: 10px; } #three { background-color: #ffe119; color: #ffe119; - top: 80px; + top: 130px; left: 10px; } @@ -45,19 +45,19 @@ background-color: #4363d8; color: #4363d8; top: 10px; - left: 45px; + left: 85px; } #five { background-color: #f58231; color: #f58231; - top: 45px; - left: 45px; + top: 70px; + left: 85px; } #six { background-color: #000075; color: #000075; - top: 80px; - left: 45px; + top: 130px; + left: 85px; } \ No newline at end of file From 8e4af952e4915460c239a95e450b4060087f255d Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Fri, 1 Nov 2024 13:29:05 +0000 Subject: [PATCH 36/57] Remove old experiment --- .../pages/Support/UserInteraction/a.test/main/headers.json | 1 - .../pages/Support/UserInteraction/a.test/main/index.html | 6 ------ .../pages/Support/UserInteraction/interaction_script.cmd | 1 - 3 files changed, 8 deletions(-) delete mode 100644 experiments/pages/Support/UserInteraction/a.test/main/headers.json delete mode 100644 experiments/pages/Support/UserInteraction/a.test/main/index.html delete mode 100644 experiments/pages/Support/UserInteraction/interaction_script.cmd diff --git a/experiments/pages/Support/UserInteraction/a.test/main/headers.json b/experiments/pages/Support/UserInteraction/a.test/main/headers.json deleted file mode 100644 index fe51488c..00000000 --- a/experiments/pages/Support/UserInteraction/a.test/main/headers.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/experiments/pages/Support/UserInteraction/a.test/main/index.html b/experiments/pages/Support/UserInteraction/a.test/main/index.html deleted file mode 100644 index ad482c63..00000000 --- a/experiments/pages/Support/UserInteraction/a.test/main/index.html +++ /dev/null @@ -1,6 +0,0 @@ - - CLICK - - \ No newline at end of file diff --git a/experiments/pages/Support/UserInteraction/interaction_script.cmd b/experiments/pages/Support/UserInteraction/interaction_script.cmd deleted file mode 100644 index 5daa56cf..00000000 --- a/experiments/pages/Support/UserInteraction/interaction_script.cmd +++ /dev/null @@ -1 +0,0 @@ -NAVIGATE https://a.test/Support/UserInteraction/main From a2e61c2b4b1fd2e5de940159b2762fff2929278d Mon Sep 17 00:00:00 2001 From: Gertjan Date: Wed, 13 Nov 2024 11:17:54 +0000 Subject: [PATCH 37/57] Fix button to add script --- bci/web/vue/src/components/poc-editor.vue | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/bci/web/vue/src/components/poc-editor.vue b/bci/web/vue/src/components/poc-editor.vue index ff9afb5b..a750fb01 100644 --- a/bci/web/vue/src/components/poc-editor.vue +++ b/bci/web/vue/src/components/poc-editor.vue @@ -232,6 +232,14 @@ .catch(() => { }); + }, + has_config() { + if (this.active_poc.tree === undefined) { + console.error("Could not check config in undefined poc tree"); + return false; + } + const arr = Object.keys(this.active_poc.tree).filter((domain) => ['url_queue.txt', 'interaction_script.cmd'].includes(domain)); + return arr.length > 0; } }, mounted() { @@ -292,9 +300,6 @@ -
-
  • - Add page -
  • +
    +
  • + Add page +
  • +
  • + Add script +
  • +
    From 792447e4384be3164169f85bc12f35c9b75efe14 Mon Sep 17 00:00:00 2001 From: Gertjan Date: Wed, 13 Nov 2024 12:10:55 +0000 Subject: [PATCH 38/57] Rename interaction_script.cmd to script.md and small button fix --- bci/evaluations/custom/custom_evaluation.py | 10 ++--- .../custom/default_files/script.cmd | 12 ++++++ bci/web/vue/src/components/poc-editor.vue | 41 +++++++++++-------- .../{interaction_script.cmd => script.cmd} | 2 +- .../Support/test}/interaction_script.cmd | 4 +- 5 files changed, 43 insertions(+), 26 deletions(-) create mode 100644 bci/evaluations/custom/default_files/script.cmd rename experiments/pages/Support/AutoGUI/{interaction_script.cmd => script.cmd} (94%) rename {bci/evaluations/custom/default_files => experiments/pages/Support/test}/interaction_script.cmd (79%) diff --git a/bci/evaluations/custom/custom_evaluation.py b/bci/evaluations/custom/custom_evaluation.py index 085abf41..e4e9ff5c 100644 --- a/bci/evaluations/custom/custom_evaluation.py +++ b/bci/evaluations/custom/custom_evaluation.py @@ -57,7 +57,7 @@ def initialize_tests_and_interactions(dir_tree: dict) -> dict: data = {} if interaction_script := CustomEvaluationFramework.__get_interaction_script(project_path, experiment): - data['interaction_script'] = interaction_script + data['script'] = interaction_script elif url_queue := CustomEvaluationFramework.__get_url_queue(project, project_path, experiment): data['url_queue'] = url_queue @@ -68,7 +68,7 @@ def initialize_tests_and_interactions(dir_tree: dict) -> dict: @staticmethod def __get_interaction_script(project_path: str, experiment: str) -> list[str] | None: - interaction_file_path = os.path.join(project_path, experiment, 'interaction_script.cmd') + interaction_file_path = os.path.join(project_path, experiment, 'script.cmd') if os.path.isfile(interaction_file_path): # If an interaction script is specified, it is parsed and used with open(interaction_file_path) as file: @@ -97,7 +97,7 @@ def __get_url_queue(project: str, project_path: str, experiment: str) -> Optiona @staticmethod def is_runnable_experiment(project: str, poc: str, dir_tree: dict[str,dict], data: dict[str,str]) -> bool: # Always runnable if there is either an interaction script or url_queue present - if 'interaction_script' in data or 'url_queue' in data: + if 'script' in data or 'url_queue' in data: return True # Should have exactly one main folder otherwise @@ -123,8 +123,8 @@ def perform_specific_evaluation(self, browser: Browser, params: TestParameters) experiment = self.tests_per_project[params.evaluation_configuration.project][params.mech_group] max_tries = 3 - if 'interaction_script' in experiment: - interaction = Interaction(browser, experiment['interaction_script'], params) + if 'script' in experiment: + interaction = Interaction(browser, experiment['script'], params) tries = 0 while tries < max_tries: tries += 1 diff --git a/bci/evaluations/custom/default_files/script.cmd b/bci/evaluations/custom/default_files/script.cmd new file mode 100644 index 00000000..749e7a21 --- /dev/null +++ b/bci/evaluations/custom/default_files/script.cmd @@ -0,0 +1,12 @@ +# TODO - add your interaction script using the commands + +# NAVIGATE url +# CLICK_POSITION x y where x and y are absolute numbers or screen percentages +# CLICK element_id where element_id is one of one, two, three, four, five, six +# WRITE text +# PRESS key +# HOLD key +# RELEASE key +# HOTKEY key1 key2 ... +# SLEEP seconds where seconds is a float or an int +# SCREENSHOT file_name diff --git a/bci/web/vue/src/components/poc-editor.vue b/bci/web/vue/src/components/poc-editor.vue index a750fb01..167e0175 100644 --- a/bci/web/vue/src/components/poc-editor.vue +++ b/bci/web/vue/src/components/poc-editor.vue @@ -31,14 +31,24 @@ // Example tree: // { // 'test.com': { - // "path1": ["file1", "file2"], - // "path2": ["file1"], + // "path1": { + // "file1": null, + // "file2": null + // }, + // "path2": { + // "file1": null + // }, // }, // 'not.test': { - // "path1": ["file1", "file2"], - // "path2": ["file1"], + // "path1": { + // "file1": null, + // "file2": null + // }, + // "path2": { + // "file1": null + // }, // }, - // 'interaction_script.cmd': 'interaction_script.cmd', + // 'script.cmd': null, // }, }, available_file_types: [ @@ -47,7 +57,7 @@ 'py', ], available_config_types: { - 'interaction_script.cmd': 'Interaction script', + 'script.cmd': 'Interaction script', 'url_queue.txt': 'URL queue' }, poc_tree_config: { @@ -238,7 +248,7 @@ console.error("Could not check config in undefined poc tree"); return false; } - const arr = Object.keys(this.active_poc.tree).filter((domain) => ['url_queue.txt', 'interaction_script.cmd'].includes(domain)); + const arr = Object.keys(this.active_poc.tree).filter((domain) => ['url_queue.txt', 'script.cmd'].includes(domain)); return arr.length > 0; } }, @@ -277,17 +287,12 @@
    - + diff --git a/bci/web/vue/src/components/gantt.vue b/bci/web/vue/src/components/gantt.vue index a23bf780..a31ae987 100644 --- a/bci/web/vue/src/components/gantt.vue +++ b/bci/web/vue/src/components/gantt.vue @@ -1,8 +1,14 @@ diff --git a/bci/web/vue/src/components/section-header.vue b/bci/web/vue/src/components/section-header.vue index 0cd95c7d..1740b38a 100644 --- a/bci/web/vue/src/components/section-header.vue +++ b/bci/web/vue/src/components/section-header.vue @@ -42,7 +42,7 @@ }, "results": { "title": "Results", - "tooltip": "Choose an experiment from the dropdown menu to visualize its results in the Gantt chart below. Squares represent (approximate) release binaries, while dots represent revision binaries. Clicking on a dot will open the web page for the associated revision in the public browser repository." + "tooltip": "Choose an experiment from the dropdown menu to visualize its results in the Gantt chart below. Squares represent (approximate) release binaries, while dots represent revision binaries. Clicking on a dot will open the web page for the associated revision in the public browser repository. Holding shift while clicking on any dot or square will delete the particular result." }, "search_strategy": { "title": "Search strategy", From c00eb5bb75707beb02cc91ba855f16bf733faade Mon Sep 17 00:00:00 2001 From: Gertjan Date: Tue, 26 Nov 2024 17:30:40 +0000 Subject: [PATCH 50/57] Fix SHIFT down checking for Gantt chart --- bci/web/vue/src/components/gantt.vue | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bci/web/vue/src/components/gantt.vue b/bci/web/vue/src/components/gantt.vue index a31ae987..fcbad9d4 100644 --- a/bci/web/vue/src/components/gantt.vue +++ b/bci/web/vue/src/components/gantt.vue @@ -19,12 +19,13 @@ export default { revision_range: null, x_min: null, x_max: null, + shift_down: false, } }, methods: { init_plot() { console.log("Initializing Gantt chart..."); - + document.addEventListener("keydown", (e) => { if (e.shiftKey) { this.shift_down = true; @@ -123,7 +124,7 @@ export default { return; } - if (this.shift_down) { + if (this.shift_down === true) { // Ask to remove datapoint this.remove_datapoint(revision_number, browser_version, type); } else { From fd30b0b606ec3fb6ca0682a42e1dc5975d6d51ad Mon Sep 17 00:00:00 2001 From: Gertjan Date: Tue, 26 Nov 2024 17:39:07 +0000 Subject: [PATCH 51/57] Isolate browser sessions and clean Downloads folder between tries. Additionally, fix bug where cached Firefox binary data was misinterpreted. --- bci/browser/configuration/browser.py | 13 +++++++++++- bci/browser/configuration/firefox.py | 2 +- bci/browser/configuration/profile.py | 16 ++++++++++----- bci/database/mongo/revision_cache.py | 2 +- bci/evaluations/custom/custom_evaluation.py | 20 +++++++++---------- bci/util.py | 10 ++++++++++ .../states/revisions/firefox.py | 2 ++ 7 files changed, 46 insertions(+), 19 deletions(-) diff --git a/bci/browser/configuration/browser.py b/bci/browser/configuration/browser.py index f6a9f2e7..6b0c86df 100644 --- a/bci/browser/configuration/browser.py +++ b/bci/browser/configuration/browser.py @@ -63,11 +63,16 @@ def post_evaluation_cleanup(self): def pre_test_setup(self): self.__prepare_execution_folder() - self._prepare_profile_folder() def post_test_cleanup(self): self.__remove_execution_folder() + + def pre_try_setup(self): + self._prepare_profile_folder() + + def post_try_cleanup(self): self.__remove_profile_folder() + self.__empty_downloads_folder() def __fetch_binary(self): self.binary.fetch_binary() @@ -87,9 +92,15 @@ def __remove_execution_folder(self): util.rmtree(self.__get_execution_folder_path()) def __remove_profile_folder(self): + if self._profile_path is None: + return remove_profile_execution_folder(self._profile_path) self._profile_path = None + def __empty_downloads_folder(self): + download_folder = '/root/Downloads' + util.remove_all_in_folder(download_folder) + def __get_execution_folder_path(self) -> str: return os.path.join(EXECUTION_PARENT_FOLDER, str(self.state.name)) diff --git a/bci/browser/configuration/firefox.py b/bci/browser/configuration/firefox.py index cff1d7f1..239934ab 100644 --- a/bci/browser/configuration/firefox.py +++ b/bci/browser/configuration/firefox.py @@ -96,7 +96,7 @@ def _prepare_profile_folder(self): if 'tp' in self.browser_config.browser_setting: self._profile_path = prepare_firefox_profile('tp-67') else: - self._profile_path = prepare_firefox_profile('default-67') + self._profile_path = prepare_firefox_profile() # Make Firefox trust the bughog CA diff --git a/bci/browser/configuration/profile.py b/bci/browser/configuration/profile.py index 520b612d..79e03d12 100644 --- a/bci/browser/configuration/profile.py +++ b/bci/browser/configuration/profile.py @@ -1,4 +1,5 @@ import os +from typing import Optional from bci import cli @@ -6,7 +7,7 @@ PROFILE_EXECUTION_FOLDER = '/tmp/profiles' -def prepare_chromium_profile(profile_name: str = None) -> str: +def prepare_chromium_profile(profile_name: Optional[str] = None) -> str: # Create new execution profile folder profile_execution_path = os.path.join(PROFILE_EXECUTION_FOLDER, 'new_profile') profile_execution_path = __create_folder(profile_execution_path) @@ -14,21 +15,26 @@ def prepare_chromium_profile(profile_name: str = None) -> str: # Copy profile from storage to execution folder if profile_name is given if profile_name: if not os.path.exists(os.path.join(PROFILE_STORAGE_FOLDER, 'chromium', profile_name)): - raise Exception(f'Profile \'{profile_name}\' does not exist') + raise Exception(f"Profile '{profile_name}' does not exist") profile_storage_path = os.path.join(PROFILE_STORAGE_FOLDER, profile_name, 'Default') cli.execute(f'cp -r {profile_storage_path} {profile_execution_path}') return profile_execution_path -def prepare_firefox_profile(profile_name: str = None) -> str: +def prepare_firefox_profile(profile_name: Optional[str] = None) -> str: + """" + Create a temporary profile folder, based on the provided name of the premade profile. + + :param profile_name: Reference to the premade profile folder used for creating the temporary profile. + """ # Create new execution profile folder profile_execution_path = os.path.join(PROFILE_EXECUTION_FOLDER, 'new_profile') profile_execution_path = __create_folder(profile_execution_path) # Copy profile from storage to execution folder if profile_name is given - if profile_name is None: + if profile_name is not None: if not os.path.exists(os.path.join(PROFILE_STORAGE_FOLDER, 'firefox', profile_name)): - raise Exception(f'Profile \'{profile_name}\' does not exist') + raise Exception(f"Profile '{profile_name}' does not exist") profile_storage_path = os.path.join(PROFILE_STORAGE_FOLDER, profile_name) cli.execute(f'cp -r {profile_storage_path} {profile_execution_path}') return profile_execution_path diff --git a/bci/database/mongo/revision_cache.py b/bci/database/mongo/revision_cache.py index 08cb5d48..6a06efda 100644 --- a/bci/database/mongo/revision_cache.py +++ b/bci/database/mongo/revision_cache.py @@ -44,7 +44,7 @@ def firefox_has_binary_for(revision_nb: Optional[int], revision_id: Optional[str @staticmethod def firefox_get_binary_info(revision_id: str) -> Optional[dict]: collection = MongoDB().get_collection('firefox_binary_availability') - return collection.find_one({'revision_id': revision_id}, {'files_url': 1, 'app_version': 1}) + return collection.find_one({'node': revision_id}, {'files_url': 1, 'app_version': 1}) @staticmethod def firefox_get_previous_and_next_revision_nb_with_binary(revision_nb: int) -> tuple[Optional[int], Optional[int]]: diff --git a/bci/evaluations/custom/custom_evaluation.py b/bci/evaluations/custom/custom_evaluation.py index 55df01c8..96a441ba 100644 --- a/bci/evaluations/custom/custom_evaluation.py +++ b/bci/evaluations/custom/custom_evaluation.py @@ -123,24 +123,22 @@ def perform_specific_evaluation(self, browser: Browser, params: TestParameters) experiment = self.tests_per_project[params.evaluation_configuration.project][params.mech_group] max_tries = 3 - if 'script' in experiment: - interaction = Interaction(browser, experiment['script'], params) - tries = 0 - while tries < max_tries: - tries += 1 + for _ in range(max_tries): + browser.pre_try_setup() + if 'script' in experiment: + interaction = Interaction(browser, experiment['script'], params) interaction.execute() - else: - url_queue = experiment['url_queue'] - for url in url_queue: - tries = 0 - while tries < max_tries: - tries += 1 + else: + url_queue = experiment['url_queue'] + for url in url_queue: browser.visit(url) + browser.post_try_cleanup() except Exception as e: logger.error(f'Error during test: {e}', exc_info=True) is_dirty = True finally: collector.stop() + results = collector.collect_results() if not is_dirty: # New way to perform sanity check diff --git a/bci/util.py b/bci/util.py index ffaf9651..3175ca1c 100644 --- a/bci/util.py +++ b/bci/util.py @@ -42,6 +42,16 @@ def copy_folder(src_path, dst_path): shutil.copytree(src_path, dst_path, dirs_exist_ok=True) +def remove_all_in_folder(folder_path: str) -> None: + for root, dirs, files in os.walk(folder_path): + for file_name in files: + file_path = os.path.join(root, file_name) + os.remove(file_path) + for dir_name in dirs: + dir_path = os.path.join(root, dir_name) + shutil.rmtree(dir_path) + + def rmtree(src_path): """ Removes folder at given src_path. diff --git a/bci/version_control/states/revisions/firefox.py b/bci/version_control/states/revisions/firefox.py index f9eb8315..928864b5 100644 --- a/bci/version_control/states/revisions/firefox.py +++ b/bci/version_control/states/revisions/firefox.py @@ -28,6 +28,8 @@ def has_online_binary(self) -> bool: def get_online_binary_url(self) -> str: result = RevisionCache.firefox_get_binary_info(self._revision_id) + if result is None: + raise AttributeError(f"Could not find binary url for '{self._revision_id}") binary_base_url = result['files_url'] app_version = result['app_version'] binary_url = f'{binary_base_url}firefox-{app_version}.en-US.linux-x86_64.tar.bz2' From 1cdecf738a6b43ad4bafa858eafa9bc94f9885ca Mon Sep 17 00:00:00 2001 From: Gertjan Date: Thu, 28 Nov 2024 08:58:57 +0000 Subject: [PATCH 52/57] Fix bug where states would be evaluated more than once Because BGB Search was not aware of states that were already in progress when BGB Sequence had finished (no database entry yet), some states would be evaluated more than once on occasion. This especially happened when there are more concurrent containers than states to evaluate. --- bci/search_strategy/bgb_search.py | 16 ++++++++++++++-- bci/search_strategy/bgb_sequence.py | 5 +++-- bci/search_strategy/composite_search.py | 13 ++++++++----- bci/search_strategy/sequence_strategy.py | 5 +++-- 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/bci/search_strategy/bgb_search.py b/bci/search_strategy/bgb_search.py index a9f6a221..34fcf674 100644 --- a/bci/search_strategy/bgb_search.py +++ b/bci/search_strategy/bgb_search.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging from typing import Optional @@ -16,13 +18,14 @@ class BiggestGapBisectionSearch(BiggestGapBisectionSequence): It stops when there are no more states to evaluate between two states with different outcomes. """ - def __init__(self, state_factory: StateFactory) -> None: + def __init__(self, state_factory: StateFactory, completed_states: Optional[list[State]]=None) -> None: """ Initializes the search strategy. :param state_factory: The factory to create new states. + :param completed_states: States that have already been returned. """ - super().__init__(state_factory, 0) + super().__init__(state_factory, 0, completed_states=completed_states) def next(self) -> State: """ @@ -86,3 +89,12 @@ def __get_next_pair_to_split(self) -> Optional[tuple[State, State]]: # splitter of the second gap with having to wait for the first gap to be fully evaluated. pairs.sort(key=lambda pair: pair[1].index - pair[0].index, reverse=True) return pairs[0] + + @staticmethod + def create_from_bgb_sequence(bgb_sequence: BiggestGapBisectionSequence) -> BiggestGapBisectionSearch: + """ + Returns a BGB search object, which continues on state of the given BGB sequence object. + + :param bgb_sequence: The BGB sequence object from which the state will be used to create the BGB search object. + """ + return BiggestGapBisectionSearch(bgb_sequence._state_factory, completed_states=bgb_sequence._completed_states) diff --git a/bci/search_strategy/bgb_sequence.py b/bci/search_strategy/bgb_sequence.py index 92a9b77f..f59efde7 100644 --- a/bci/search_strategy/bgb_sequence.py +++ b/bci/search_strategy/bgb_sequence.py @@ -13,14 +13,15 @@ class BiggestGapBisectionSequence(SequenceStrategy): This sequence strategy will split the biggest gap between two states in half and return the state in the middle. """ - def __init__(self, state_factory: StateFactory, limit: int) -> None: + def __init__(self, state_factory: StateFactory, limit: int, completed_states: Optional[list[State]]=None) -> None: """ Initializes the sequence strategy. :param state_factory: The factory to create new states. :param limit: The maximum number of states to evaluate. 0 means no limit. + :param completed_states: States that have already been returned. """ - super().__init__(state_factory, limit) + super().__init__(state_factory, limit, completed_states=completed_states) self._unavailability_gap_pairs: set[tuple[State, State]] = set() """Tuples in this list are **strict** boundaries of ranges without any available binaries.""" diff --git a/bci/search_strategy/composite_search.py b/bci/search_strategy/composite_search.py index f59a7c10..90625e63 100644 --- a/bci/search_strategy/composite_search.py +++ b/bci/search_strategy/composite_search.py @@ -8,16 +8,19 @@ class CompositeSearch(): def __init__(self, state_factory: StateFactory, sequence_limit: int) -> None: self.sequence_strategy = BiggestGapBisectionSequence(state_factory, limit=sequence_limit) - self.search_strategy = BiggestGapBisectionSearch(state_factory) - self.sequence_strategy_finished = False + self.search_strategy = None def next(self) -> State: - # First we use the sequence strategy to select the next state - if not self.sequence_strategy_finished: + """ + Returns the next state, based on a sequence strategy and search strategy. + First, the sequence strategy decides which state to return until it returns the SequenceFinished exception. + From then on, the search strategy decides which state to return. + """ + if self.search_strategy is None: try: return self.sequence_strategy.next() except SequenceFinished: - self.sequence_strategy_finished = True + self.search_strategy = BiggestGapBisectionSearch.create_from_bgb_sequence(self.sequence_strategy) return self.search_strategy.next() else: return self.search_strategy.next() diff --git a/bci/search_strategy/sequence_strategy.py b/bci/search_strategy/sequence_strategy.py index 3a6eaff4..c93585fa 100644 --- a/bci/search_strategy/sequence_strategy.py +++ b/bci/search_strategy/sequence_strategy.py @@ -10,17 +10,18 @@ class SequenceStrategy: - def __init__(self, state_factory: StateFactory, limit) -> None: + def __init__(self, state_factory: StateFactory, limit: int, completed_states: Optional[list[State]]=None) -> None: """ Initializes the sequence strategy. :param state_factory: The factory to create new states. :param limit: The maximum number of states to evaluate. 0 means no limit. + :param completed_states: States that have already been returned. """ self._state_factory = state_factory self._limit = limit self._lower_state, self._upper_state = self.__create_available_boundary_states() - self._completed_states = [] + self._completed_states = completed_states if completed_states else [] @abstractmethod def next(self) -> State: From 0ebaf3ce44315ccfa46627b8094cd6ff2a410296 Mon Sep 17 00:00:00 2001 From: Gertjan Date: Thu, 28 Nov 2024 09:05:44 +0000 Subject: [PATCH 53/57] Update BokehJS --- bci/web/vue/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bci/web/vue/index.html b/bci/web/vue/index.html index a427ba2d..6bf8ba4b 100644 --- a/bci/web/vue/index.html +++ b/bci/web/vue/index.html @@ -4,8 +4,8 @@ - - + + BugHog From c5e563e2448b49195184a305b261cd257516df5f Mon Sep 17 00:00:00 2001 From: Gertjan Date: Thu, 28 Nov 2024 09:13:19 +0000 Subject: [PATCH 54/57] Upgrade python dependencies --- requirements.txt | 14 ++++++---- requirements_dev.txt | 66 ++++++++++++++++++++++++++++++++++++-------- 2 files changed, 63 insertions(+), 17 deletions(-) diff --git a/requirements.txt b/requirements.txt index 2a367b96..10050f30 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ # -# This file is autogenerated by pip-compile with Python 3.12 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # pip-compile requirements.in # -blinker==1.8.2 +blinker==1.9.0 # via flask certifi==2024.8.30 # via requests @@ -16,7 +16,7 @@ dnspython==2.7.0 # via pymongo docker==7.1.0 # via -r requirements.in -flask==3.0.3 +flask==3.1.0 # via # -r requirements.in # flask-sock @@ -40,10 +40,12 @@ markupsafe==3.0.2 # werkzeug mouseinfo==0.1.3 # via pyautogui -packaging==24.1 +packaging==24.2 # via gunicorn pillow==11.0.0 - # via -r requirements.in + # via + # -r requirements.in + # pyscreeze pyautogui==0.9.54 # via -r requirements.in pygetwindow==0.0.9 @@ -80,7 +82,7 @@ urllib3==2.2.3 # via # docker # requests -werkzeug==3.0.4 +werkzeug==3.1.3 # via flask wsproto==1.2.0 # via simple-websocket diff --git a/requirements_dev.txt b/requirements_dev.txt index c9143ac6..3ef374bc 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -10,13 +10,13 @@ astroid==3.3.5 # via pylint autopep8==2.3.1 # via -r requirements_dev.in -blinker==1.8.2 +blinker==1.9.0 # via # -r requirements.txt # flask -boto3==1.35.45 +boto3==1.35.71 # via -r requirements_dev.in -botocore==1.35.45 +botocore==1.35.71 # via # -r requirements_dev.in # boto3 @@ -33,11 +33,11 @@ click==8.1.7 # via # -r requirements.txt # flask -coverage[toml]==7.6.4 +coverage[toml]==7.6.8 # via # -r requirements_dev.in # pytest-cov -debugpy==1.8.7 +debugpy==1.8.9 # via -r requirements_dev.in dill==0.3.9 # via pylint @@ -51,7 +51,7 @@ flake8==7.1.1 # via # -r requirements_dev.in # pytest-flake8 -flask==3.0.3 +flask==3.1.0 # via # -r requirements.txt # flask-sock @@ -96,42 +96,83 @@ mccabe==0.7.0 # via # flake8 # pylint -packaging==24.1 +mouseinfo==0.1.3 + # via + # -r requirements.txt + # pyautogui +packaging==24.2 # via # -r requirements.txt # anybadge # gunicorn # pytest +pillow==11.0.0 + # via + # -r requirements.txt + # pyscreeze platformdirs==4.3.6 # via pylint pluggy==1.5.0 # via pytest +pyautogui==0.9.54 + # via -r requirements.txt pycodestyle==2.12.1 # via # autopep8 # flake8 pyflakes==3.2.0 # via flake8 +pygetwindow==0.0.9 + # via + # -r requirements.txt + # pyautogui pylint==3.3.1 # via -r requirements_dev.in pymongo==4.10.1 # via -r requirements.txt +pymsgbox==1.0.9 + # via + # -r requirements.txt + # pyautogui +pyperclip==1.9.0 + # via + # -r requirements.txt + # mouseinfo +pyrect==0.2.0 + # via + # -r requirements.txt + # pygetwindow +pyscreeze==1.0.1 + # via + # -r requirements.txt + # pyautogui pytest==8.3.3 # via # -r requirements_dev.in # pytest-cov # pytest-flake8 -pytest-cov==5.0.0 +pytest-cov==6.0.0 # via -r requirements_dev.in -pytest-flake8==1.2.1 +pytest-flake8==1.3.0 # via -r requirements_dev.in python-dateutil==2.9.0.post0 # via botocore +python3-xlib==0.15 + # via + # -r requirements.txt + # mouseinfo + # pyautogui +pytweening==1.2.0 + # via + # -r requirements.txt + # pyautogui +pyvirtualdisplay==3.0 + # via -r requirements.txt requests==2.32.3 # via # -r requirements.txt # docker -s3transfer==0.10.3 +s3transfer==0.10.4 # via boto3 simple-websocket==1.1.0 # via @@ -142,6 +183,7 @@ six==1.16.0 # -r requirements.txt # flatten-dict # python-dateutil + # xlib tomlkit==0.13.2 # via pylint urllib3==2.2.3 @@ -150,7 +192,7 @@ urllib3==2.2.3 # botocore # docker # requests -werkzeug==3.0.4 +werkzeug==3.1.3 # via # -r requirements.txt # flask @@ -158,3 +200,5 @@ wsproto==1.2.0 # via # -r requirements.txt # simple-websocket +xlib==0.21 + # via -r requirements.txt From 4a9a5a5e2c9b864cf3249c8a2642c2728767d33e Mon Sep 17 00:00:00 2001 From: Gertjan Date: Thu, 28 Nov 2024 09:42:12 +0000 Subject: [PATCH 55/57] Add convenience scripts for updating pip and npm dependencies --- scripts/node_update.sh | 3 +++ scripts/pip_update.sh | 4 ++++ 2 files changed, 7 insertions(+) create mode 100755 scripts/node_update.sh create mode 100755 scripts/pip_update.sh diff --git a/scripts/node_update.sh b/scripts/node_update.sh new file mode 100755 index 00000000..24cd6576 --- /dev/null +++ b/scripts/node_update.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +exec docker run -v ${PWD}/bci/web/vue/:/app -w /app node:lts-alpine npm update diff --git a/scripts/pip_update.sh b/scripts/pip_update.sh new file mode 100755 index 00000000..150c577f --- /dev/null +++ b/scripts/pip_update.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +pip-compile -U requirements.in +pip-compile -U requirements_dev.in From 2d19a14345215141f900a9c5ac7ab380ac628a05 Mon Sep 17 00:00:00 2001 From: Gertjan Date: Thu, 28 Nov 2024 09:43:42 +0000 Subject: [PATCH 56/57] Update node dependencies --- bci/web/vue/package-lock.json | 450 ++++++++++++++++++++++++---------- 1 file changed, 317 insertions(+), 133 deletions(-) diff --git a/bci/web/vue/package-lock.json b/bci/web/vue/package-lock.json index 1a1d035c..e393c6a9 100644 --- a/bci/web/vue/package-lock.json +++ b/bci/web/vue/package-lock.json @@ -29,6 +29,7 @@ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -40,6 +41,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -48,16 +50,18 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.9.tgz", - "integrity": "sha512-aI3jjAAO1fh7vY/pBGsn1i9LDbRP43+asrRlkPuTXW5yHXtd1NgTEMudbBoDDxrf1daEEfPJqR+JBMakzrR4Dg==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", + "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.26.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -67,9 +71,10 @@ } }, "node_modules/@babel/types": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.9.tgz", - "integrity": "sha512-OwS2CM5KocvQ/k7dFJa8i5bNGJP0hXWfVCfDkqRFP1IreH1JDC7wG6eCYCi0+McbfT8OR/kNqsI0UU0xP9H6PQ==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", + "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" @@ -86,6 +91,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -102,6 +108,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -118,6 +125,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -134,6 +142,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -150,6 +159,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -166,6 +176,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -182,6 +193,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -198,6 +210,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -214,6 +227,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -230,6 +244,7 @@ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -246,6 +261,7 @@ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -262,6 +278,7 @@ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -278,6 +295,7 @@ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -294,6 +312,7 @@ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -310,6 +329,7 @@ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -326,6 +346,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -342,6 +363,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -358,6 +380,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -374,6 +397,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" @@ -390,6 +414,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -406,6 +431,7 @@ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -422,6 +448,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -435,6 +462,7 @@ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -452,6 +480,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -466,6 +495,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -475,6 +505,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -482,13 +513,15 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -499,6 +532,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -512,6 +546,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -521,6 +556,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -534,6 +570,7 @@ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, + "license": "MIT", "optional": true, "engines": { "node": ">=14" @@ -543,6 +580,7 @@ "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -553,6 +591,7 @@ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz", "integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==", "dev": true, + "license": "MIT", "engines": { "node": "^14.18.0 || >=16.0.0" }, @@ -562,111 +601,123 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.12.tgz", - "integrity": "sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", + "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", + "license": "MIT", "dependencies": { "@babel/parser": "^7.25.3", - "@vue/shared": "3.5.12", + "@vue/shared": "3.5.13", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.0" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.12.tgz", - "integrity": "sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", + "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", + "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.12", - "@vue/shared": "3.5.12" + "@vue/compiler-core": "3.5.13", + "@vue/shared": "3.5.13" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.12.tgz", - "integrity": "sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", + "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", + "license": "MIT", "dependencies": { "@babel/parser": "^7.25.3", - "@vue/compiler-core": "3.5.12", - "@vue/compiler-dom": "3.5.12", - "@vue/compiler-ssr": "3.5.12", - "@vue/shared": "3.5.12", + "@vue/compiler-core": "3.5.13", + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13", "estree-walker": "^2.0.2", "magic-string": "^0.30.11", - "postcss": "^8.4.47", + "postcss": "^8.4.48", "source-map-js": "^1.2.0" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.12.tgz", - "integrity": "sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", + "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", + "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.12", - "@vue/shared": "3.5.12" + "@vue/compiler-dom": "3.5.13", + "@vue/shared": "3.5.13" } }, "node_modules/@vue/reactivity": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.12.tgz", - "integrity": "sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", + "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", + "license": "MIT", "dependencies": { - "@vue/shared": "3.5.12" + "@vue/shared": "3.5.13" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.12.tgz", - "integrity": "sha512-hrMUYV6tpocr3TL3Ad8DqxOdpDe4zuQY4HPY3X/VRh+L2myQO8MFXPAMarIOSGNu0bFAjh1yBkMPXZBqCk62Uw==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz", + "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", + "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.12", - "@vue/shared": "3.5.12" + "@vue/reactivity": "3.5.13", + "@vue/shared": "3.5.13" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.12.tgz", - "integrity": "sha512-q8VFxR9A2MRfBr6/55Q3umyoN7ya836FzRXajPB6/Vvuv0zOPL+qltd9rIMzG/DbRLAIlREmnLsplEF/kotXKA==", - "dependencies": { - "@vue/reactivity": "3.5.12", - "@vue/runtime-core": "3.5.12", - "@vue/shared": "3.5.12", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", + "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.13", + "@vue/runtime-core": "3.5.13", + "@vue/shared": "3.5.13", "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.12.tgz", - "integrity": "sha512-I3QoeDDeEPZm8yR28JtY+rk880Oqmj43hreIBVTicisFTx/Dl7JpG72g/X7YF8hnQD3IFhkky5i2bPonwrTVPg==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz", + "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", + "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.12", - "@vue/shared": "3.5.12" + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13" }, "peerDependencies": { - "vue": "3.5.12" + "vue": "3.5.13" } }, "node_modules/@vue/shared": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.12.tgz", - "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==" + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", + "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", + "license": "MIT" }, "node_modules/@vueform/slider": { "version": "2.1.10", "resolved": "https://registry.npmjs.org/@vueform/slider/-/slider-2.1.10.tgz", - "integrity": "sha512-L2G3Ju51Yq6yWF2wzYYsicUUaH56kL1QKGVtimUVHT1K1ADcRT94xVyIeJpS0klliVEeF6iMZFbdXtHq8AsDHw==" + "integrity": "sha512-L2G3Ju51Yq6yWF2wzYYsicUUaH56kL1QKGVtimUVHT1K1ADcRT94xVyIeJpS0klliVEeF6iMZFbdXtHq8AsDHw==", + "license": "MIT" }, "node_modules/ace-builds": { - "version": "1.36.3", - "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.36.3.tgz", - "integrity": "sha512-YcdwV2IIaJSfjkWAR1NEYN5IxBiXefTgwXsJ//UlaFrjXDX5hQpvPFvEePHz2ZBUfvO54RjHeRUQGX8MS5HaMQ==" + "version": "1.36.5", + "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.36.5.tgz", + "integrity": "sha512-mZ5KVanRT6nLRDLqtG/1YQQLX/gZVC/v526cm1Ru/MTSlrbweSmqv2ZT0d2GaHpJq035MwCMIrj+LgDAUnDXrg==", + "license": "BSD-3-Clause" }, "node_modules/ansi-regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -679,6 +730,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -690,13 +742,15 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, + "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -709,12 +763,14 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" }, "node_modules/autoprefixer": { "version": "10.4.20", @@ -735,6 +791,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "browserslist": "^4.23.3", "caniuse-lite": "^1.0.30001646", @@ -754,9 +811,10 @@ } }, "node_modules/axios": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "version": "1.7.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.8.tgz", + "integrity": "sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -767,13 +825,15 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -786,6 +846,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -795,6 +856,7 @@ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -821,6 +883,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "caniuse-lite": "^1.0.30001669", "electron-to-chromium": "^1.5.41", @@ -839,14 +902,15 @@ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/caniuse-lite": { - "version": "1.0.30001669", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz", - "integrity": "sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==", + "version": "1.0.30001684", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001684.tgz", + "integrity": "sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ==", "dev": true, "funding": [ { @@ -861,13 +925,15 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, + "license": "MIT", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -892,6 +958,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -904,6 +971,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -915,12 +983,14 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -933,15 +1003,17 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -956,6 +1028,7 @@ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true, + "license": "MIT", "bin": { "cssesc": "bin/cssesc" }, @@ -966,12 +1039,14 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -980,36 +1055,42 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.43", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.43.tgz", - "integrity": "sha512-NxnmFBHDl5Sachd2P46O7UJiMaMHMLSofoIWVJq3mj8NJgG0umiSeljAVP9lGzjI0UDLJJ5jjoGjcrB8RSbjLQ==", - "dev": true + "version": "1.5.66", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.66.tgz", + "integrity": "sha512-pI2QF6+i+zjPbqRzJwkMvtvkdI7MjVbSh2g8dlMguDJIXEPw+kwasS1Jl+YGPEBfGVxsVgGUratAKymPdPo2vQ==", + "dev": true, + "license": "ISC" }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", "engines": { "node": ">=0.12" }, @@ -1023,6 +1104,7 @@ "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -1059,6 +1141,7 @@ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1066,13 +1149,15 @@ "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -1089,6 +1174,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -1101,6 +1187,7 @@ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } @@ -1110,6 +1197,7 @@ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -1121,6 +1209,7 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/flowbite/-/flowbite-1.8.1.tgz", "integrity": "sha512-lXTcO8a6dRTPFpINyOLcATCN/pK1Of/jY4PryklPllAiqH64tSDUsOdQpar3TO59ZXWwugm2e92oaqwH6X90Xg==", + "license": "MIT", "dependencies": { "@popperjs/core": "^2.9.3", "mini-svg-data-uri": "^1.4.3" @@ -1136,6 +1225,7 @@ "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -1150,6 +1240,7 @@ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", "dev": true, + "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -1165,6 +1256,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -1179,6 +1271,7 @@ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", "dev": true, + "license": "MIT", "engines": { "node": "*" }, @@ -1193,6 +1286,7 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1206,6 +1300,7 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -1215,6 +1310,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -1235,6 +1331,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -1247,6 +1344,7 @@ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -1259,6 +1357,7 @@ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, + "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -1271,6 +1370,7 @@ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dev": true, + "license": "MIT", "dependencies": { "hasown": "^2.0.2" }, @@ -1286,6 +1386,7 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1295,6 +1396,7 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1304,6 +1406,7 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -1316,6 +1419,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -1324,13 +1428,15 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -1346,6 +1452,7 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", "dev": true, + "license": "MIT", "bin": { "jiti": "bin/jiti.js" } @@ -1355,6 +1462,7 @@ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" } @@ -1363,18 +1471,21 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/magic-string": { - "version": "0.30.12", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", - "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", + "version": "0.30.14", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.14.tgz", + "integrity": "sha512-5c99P1WKTed11ZC0HMJOj6CDIue6F8ySu+bJL+85q1zBEIY8IklrJ1eiKC2NDRh3Ct3FcvmJPyQHb9erXMTJNw==", + "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } @@ -1384,6 +1495,7 @@ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -1393,6 +1505,7 @@ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -1405,6 +1518,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1413,6 +1527,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -1424,6 +1539,7 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "license": "MIT", "bin": { "mini-svg-data-uri": "cli.js" } @@ -1433,6 +1549,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -1448,6 +1565,7 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, + "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } @@ -1457,6 +1575,7 @@ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "dev": true, + "license": "MIT", "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", @@ -1464,15 +1583,16 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -1484,13 +1604,15 @@ "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1500,6 +1622,7 @@ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1509,6 +1632,7 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1518,6 +1642,7 @@ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } @@ -1526,6 +1651,7 @@ "version": "1.0.0-rc3", "resolved": "https://registry.npmjs.org/oh-vue-icons/-/oh-vue-icons-1.0.0-rc3.tgz", "integrity": "sha512-+k2YC6piK7sEZnwbkQF3UokFPMmgqpiLP6f/H0ovQFLl20QA5V4U8EcI6EclD2Lt5NMQ3k6ilLGo8XyXqdVSvg==", + "license": "MIT", "dependencies": { "vue-demi": "^0.12.5" }, @@ -1544,6 +1670,7 @@ "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.12.5.tgz", "integrity": "sha512-BREuTgTYlUr0zw0EZn3hnhC3I6gPWv+Kwh4MCih6QcAeaTlaIX0DwOVN0wHej7hSvDPecz4jygy/idsgKfW58Q==", "hasInstallScript": true, + "license": "MIT", "bin": { "vue-demi-fix": "bin/vue-demi-fix.js", "vue-demi-switch": "bin/vue-demi-switch.js" @@ -1568,13 +1695,15 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true + "dev": true, + "license": "BlueOak-1.0.0" }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1583,13 +1712,15 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -1604,13 +1735,15 @@ "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -1623,6 +1756,7 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1632,14 +1766,15 @@ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "funding": [ { "type": "opencollective", @@ -1654,9 +1789,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.1.0", + "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { @@ -1668,6 +1804,7 @@ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", "dev": true, + "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", @@ -1685,6 +1822,7 @@ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", "dev": true, + "license": "MIT", "dependencies": { "camelcase-css": "^2.0.1" }, @@ -1714,6 +1852,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "lilconfig": "^3.0.0", "yaml": "^2.3.4" @@ -1739,6 +1878,7 @@ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", "dev": true, + "license": "MIT", "engines": { "node": ">=14" }, @@ -1761,6 +1901,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "postcss-selector-parser": "^6.1.1" }, @@ -1776,6 +1917,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dev": true, + "license": "MIT", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -1788,12 +1930,14 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" }, "node_modules/queue-microtask": { "version": "1.2.3", @@ -1813,13 +1957,15 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", "dev": true, + "license": "MIT", "dependencies": { "pify": "^2.3.0" } @@ -1829,6 +1975,7 @@ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, + "license": "MIT", "dependencies": { "picomatch": "^2.2.1" }, @@ -1841,6 +1988,7 @@ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dev": true, + "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -1858,6 +2006,7 @@ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true, + "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -1868,6 +2017,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", "dev": true, + "license": "MIT", "bin": { "rollup": "dist/bin/rollup" }, @@ -1898,6 +2048,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } @@ -1907,6 +2058,7 @@ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -1919,6 +2071,7 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1928,6 +2081,7 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, + "license": "ISC", "engines": { "node": ">=14" }, @@ -1939,6 +2093,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -1948,6 +2103,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, + "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -1966,6 +2122,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -1980,6 +2137,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1988,13 +2146,15 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2007,6 +2167,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -2023,6 +2184,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2035,6 +2197,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2044,6 +2207,7 @@ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", @@ -2066,6 +2230,7 @@ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -2074,33 +2239,34 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.14", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.14.tgz", - "integrity": "sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==", + "version": "3.4.15", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.15.tgz", + "integrity": "sha512-r4MeXnfBmSOuKUWmXe6h2CcyfzJCEk4F0pptO5jlnYSIViUkVmsawj80N5h2lO3gwcmSb4n3PuN+e+GC1Guylw==", "dev": true, + "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", - "chokidar": "^3.5.3", + "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", - "fast-glob": "^3.3.0", + "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.21.0", + "jiti": "^1.21.6", "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", + "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", @@ -2115,6 +2281,7 @@ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", "dev": true, + "license": "MIT", "dependencies": { "any-promise": "^1.0.0" } @@ -2124,6 +2291,7 @@ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", "dev": true, + "license": "MIT", "dependencies": { "thenify": ">= 3.1.0 < 4" }, @@ -2136,6 +2304,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -2147,7 +2316,8 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/update-browserslist-db": { "version": "1.1.1", @@ -2168,6 +2338,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.0" @@ -2183,13 +2354,15 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/vite": { "version": "4.5.5", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.5.tgz", "integrity": "sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ==", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", @@ -2241,15 +2414,16 @@ } }, "node_modules/vue": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.12.tgz", - "integrity": "sha512-CLVZtXtn2ItBIi/zHZ0Sg1Xkb7+PU32bJJ8Bmy7ts3jxXTcbfsEfBivFYYWz1Hur+lalqGAh65Coin0r+HRUfg==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", + "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", + "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.12", - "@vue/compiler-sfc": "3.5.12", - "@vue/runtime-dom": "3.5.12", - "@vue/server-renderer": "3.5.12", - "@vue/shared": "3.5.12" + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-sfc": "3.5.13", + "@vue/runtime-dom": "3.5.13", + "@vue/server-renderer": "3.5.13", + "@vue/shared": "3.5.13" }, "peerDependencies": { "typescript": "*" @@ -2264,6 +2438,7 @@ "version": "2.1.9", "resolved": "https://registry.npmjs.org/vue-multiselect/-/vue-multiselect-2.1.9.tgz", "integrity": "sha512-nGEppmzhQQT2iDz4cl+ZCX3BpeNhygK50zWFTIRS+r7K7i61uWXJWSioMuf+V/161EPQjexI8NaEBdUlF3dp+g==", + "license": "MIT", "engines": { "node": ">= 4.0.0", "npm": ">= 3.0.0" @@ -2274,6 +2449,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -2289,6 +2465,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -2307,6 +2484,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -2324,6 +2502,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2333,6 +2512,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -2347,13 +2527,15 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -2368,6 +2550,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2376,10 +2559,11 @@ } }, "node_modules/yaml": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", - "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", + "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", "dev": true, + "license": "ISC", "bin": { "yaml": "bin.mjs" }, From 8dcb3f079118f6a12ccc686fbd65e436234f2e25 Mon Sep 17 00:00:00 2001 From: Gertjan Date: Thu, 28 Nov 2024 10:51:41 +0000 Subject: [PATCH 57/57] Fix test failing due to changed internals --- test/sequence/test_composite_search.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/sequence/test_composite_search.py b/test/sequence/test_composite_search.py index 5a7d247a..8728c8f3 100644 --- a/test/sequence/test_composite_search.py +++ b/test/sequence/test_composite_search.py @@ -23,7 +23,6 @@ def test_binary_sequence_always_available_composite(self): outcome_func=lambda x: True if x < 50 else False, evaluated_indexes=[0, 99, 49, 74, 24, 36, 61, 86, 12, 42] ) - sequence.search_strategy._state_factory = state_factory # Sequence index_sequence = [sequence.next().index for _ in range(3)] @@ -47,7 +46,6 @@ def test_binary_sequence_always_available_composite_two_shifts(self): outcome_func=lambda x: True if x < 33 or 81 < x else False, evaluated_indexes=[0, 99, 49, 74, 24, 36, 61, 86, 12, 42] ) - sequence.search_strategy._state_factory = state_factory while True: try: @@ -57,7 +55,7 @@ def test_binary_sequence_always_available_composite_two_shifts(self): evaluated_indexes = [state.index for state in sequence.search_strategy._completed_states] - assert sequence.sequence_strategy_finished + assert sequence.search_strategy is not None assert 32 in evaluated_indexes assert 33 in evaluated_indexes assert 81 in evaluated_indexes