diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 05850e9..0000000 --- a/.dockerignore +++ /dev/null @@ -1,11 +0,0 @@ -.gitignore -.git -.DS_Store -.idea -.dockerignore -venv -docs -doc_generator -gen_docs.sh -README.md -Dockerfile \ No newline at end of file diff --git a/.gitignore b/.gitignore index f85f6dd..6f33716 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ .env -*.yaml -*.json .idea venv -__pycache__ -build +__pycache__/ +_build/ +fileio diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8d4792a..414d611 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: additional_dependencies: - flake8-docstrings - flake8-sfs - args: [--max-line-length=120, --extend-ignore=SFS3 D107 SFS301 D100 D104 D401] + args: [--max-line-length=120, --extend-ignore=SFS3 D107 D100 D104 D401 SFS101 SFS201] - repo: https://github.com/pre-commit/mirrors-isort diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 20c9ca8..0000000 --- a/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM python:3.9-slim - -ENV DOCKER=1 - -RUN mkdir /opt/netscan -COPY . /opt/netscan - -RUN cd /opt/netscan && pip3 install --user -r requirements.txt - -WORKDIR /opt/netscan - -ENTRYPOINT ["/usr/local/bin/python", "./analyzer.py"] \ No newline at end of file diff --git a/README.md b/README.md index 57f15ed..81dc153 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,43 @@ # NetScan Network Scanner to analyze devices connecting to the router and alert accordingly. -NetScan displays the connected devices, Wi-Fi signal info, speed, etc. - -This app can display intruders IP addresses, MAC addresses, and lets the user Ping the device, and even Block user from -Wi-Fi. +This app can display intruders' IP addresses, MAC addresses, and lets the user Ping the device, and even Block the device. Block IP Address feature helps the user to remove the specified connections and block the specific IP address. -### Docker Setup: +> Blocking device feature is currently available only for `Netgear` router users. + +## Coding Standards +Docstring format: [`Google`](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
+Styling conventions: [`PEP 8`](https://www.python.org/dev/peps/pep-0008/)
+Clean code with pre-commit hooks: [`flake8`](https://flake8.pycqa.org/en/latest/) and +[`isort`](https://pycqa.github.io/isort/) + +## [Release Notes](https://github.com/thevickypedia/netscan/blob/master/release_notes.rst) +**Requirement** +```shell +python -m pip install changelog-generator +``` + +**Usage** +```shell +changelog reverse -f release_notes.rst -t 'Release Notes' +``` -###### Commands: -- `docker build --progress plain -t netscan .` -- `docker run netscan` +## Linting +`PreCommit` will ensure linting, and the doc creation are run on every commit. -[Additional options that can be added to the `docker build` command.](https://docs.docker.com/engine/reference/commandline/build/#options) +**Requirement** +```shell +pip install sphinx==5.1.1 pre-commit recommonmark +``` -###### [Reference](https://github.com/MatMaul/pynetgear) +**Usage** +```shell +pre-commit run --all-files +``` -###### [Runbook](https://thevickypedia.github.io/netscan/) +## Runbook +[![made-with-sphinx-doc](https://img.shields.io/badge/Code%20Docs-Sphinx-1f425f.svg)](https://www.sphinx-doc.org/en/master/man/sphinx-autogen.html) -###### [Repository](https://github.com/thevickypedia/netscan) +[https://thevickypedia.github.io/netscan/](https://thevickypedia.github.io/netscan/) diff --git a/analyzer.py b/analyzer.py index 1fc25ce..da3d3dd 100644 --- a/analyzer.py +++ b/analyzer.py @@ -1,323 +1,31 @@ -import importlib -import json -import logging -import os -import platform -import subprocess -import time -from datetime import datetime, timezone -from pathlib import PurePath -from typing import AnyStr, NoReturn, Union +from typing import NoReturn -import yaml -from dotenv import load_dotenv -from gmailconnector.send_sms import Messenger -from pynetgear import Device, Netgear +from modules import att, models, netgear -importlib.reload(logging) -LOGGER = logging.getLogger(name=PurePath(__file__).stem) -log_formatter = logging.Formatter( - fmt="%(asctime)s - [%(levelname)s] - %(name)s - %(funcName)s - Line: %(lineno)d - %(message)s", - datefmt='%b-%d-%Y %H:%M:%S' -) -handler = logging.StreamHandler() -handler.setFormatter(fmt=log_formatter) -LOGGER.setLevel(level=logging.INFO) -LOGGER.addHandler(hdlr=handler) - -def custom_time(*args: logging.Formatter or time.time) -> time.struct_time: - """Creates custom timezone for ``logging`` which gets used only when invoked by ``Docker``. - - This is used only when triggered within a ``docker container`` as it uses UTC timezone. - - Args: - *args: Takes ``Formatter`` object and current epoch time as arguments passed by ``formatTime`` from ``logging``. - - Returns: - struct_time: - A struct_time object which is a tuple of: - **current year, month, day, hour, minute, second, weekday, year day and dst** *(Daylight Saving Time)* - """ - LOGGER.debug(args) - local_timezone = datetime.now(tz=timezone.utc).astimezone().tzinfo - return datetime.now().astimezone(tz=local_timezone).timetuple() - - -def extract_str(input_: AnyStr) -> str: - """Extracts strings from the received input. +def network_monitor(module: models.SupportedModules, init: bool = True) -> NoReturn: + """Monitor devices connected to the network. Args: - input_: Takes a string as argument. - - Returns: - str: - A string after removing special characters. - """ - return "".join([i for i in input_ if not i.isdigit() and i not in [",", ".", "?", "-", ";", "!", ":"]]).strip() - - -def device_name() -> str: - """Gets the device name for MacOS and Windows.""" - if platform.system() == 'Darwin': - system_kernel = subprocess.check_output("sysctl hw.model", shell=True).decode('utf-8').splitlines() - return extract_str(system_kernel[0].split(':')[1]) - elif platform.system() == 'Windows': - return subprocess.getoutput("WMIC CSPRODUCT GET VENDOR").replace('Vendor', '').strip() - - -def get_ssid() -> Union[str, None]: - """Checks the current operating system and runs the appropriate command to get the SSID of the access point. - - Returns: - str: - SSID of the access point/router which is being accessed. + module: Module to scan. Currently, supports any network on a Netgear router or At&t networks. + init: Takes a boolean value to create a snapshot file or actually monitor the network. """ - if platform.system() == 'Darwin': - process = subprocess.Popen( - ["/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport", "-I"], - stdout=subprocess.PIPE - ) - out, err = process.communicate() - if out.decode(encoding='UTF-8').strip() == "AirPort: Off": - LOGGER.warning(f"{device_name()} WiFi is turned off.") - return - if error := process.returncode: - LOGGER.error(f"Failed to fetch SSID with exit code: {error}\n{err}") - return - # noinspection PyTypeChecker - return dict(map(str.strip, info.split(": ")) for info in out.decode("utf-8").splitlines()[:-1] if - len(info.split()) == 2).get("SSID") - elif platform.system() == 'Windows': - netsh = subprocess.check_output("netsh wlan show interfaces", shell=True) - for info in netsh.decode('utf-8').split('\n')[:-1]: - if 'SSID' in info: - return info.strip('SSID').replace('SSID', '').replace(':', '').strip() - - -def send_sms(msg: str) -> NoReturn: - """Sens an SMS notification when invoked by the ``run`` method. - - Args: - msg: Message that has to be sent. - """ - if os.environ.get('gmail_user') and os.environ.get('gmail_pass') and os.environ.get('phone'): - messenger = Messenger(gmail_user=os.environ.get('gmail_user'), gmail_pass=os.environ.get('gmail_pass'), - phone=os.environ.get('phone'), subject="Cyber Alert", message=msg) - response = messenger.send_sms() - if response.ok: - LOGGER.info(f"Firewall alert has been sent to {os.environ.get('phone')}") + if module == models.SupportedModules.netgear: + if init: + netgear.LocalIPScan().create_snapshot() else: - LOGGER.error(f"Failed to send a notification.\n{response.body}") - - -class LocalIPScan: - """Connector to scan devices in the same IP range using ``Netgear API``. - - >>> LocalIPScan - - """ - - def __init__(self, router_pass: str = None): - """Gets local host devices connected to the same network range. - - Args: - router_pass: Password to authenticate the API client. - """ - env_file_path = '.env' - load_dotenv(dotenv_path=env_file_path) - if not router_pass: - if not (router_pass := os.environ.get('router_pass')): - raise ValueError( - 'Router password is required.' - ) - self.ssid = get_ssid() or 'your Network.' - self.snapshot = 'snapshot.json' - self.blocked = 'blocked.yaml' - self.netgear = Netgear(password=router_pass) - - def _get_devices(self) -> Device: - """Scans the Netgear router for connected devices and the devices' information. - - Returns: - Device: - Returns list of devices connected to the router and the connection information. - """ - LOGGER.info(f'Getting devices connected to {self.ssid}') - return self.netgear.get_attached_devices() - - def create_snapshot(self) -> NoReturn: - """Creates a snapshot.json which is used to determine the known and unknown devices.""" - LOGGER.warning(f"Creating a snapshot will capture the current list of devices connected to {self.ssid} at" - " this moment.") - LOGGER.warning("This capture will be used to alert/block when new devices are connected. So, " - f"please review the {self.snapshot} manually and remove the devices that aren't recognized.") - devices = {} - for device in self._get_devices(): - devices.update({str(device.ip): [str(device.name), str(device.type), str(device.allow_or_block)]}) - LOGGER.info(f'Number of devices connected: {len(list(devices.keys()))}') - with open(self.snapshot, 'w') as file: - json.dump(devices, file, indent=2) - - def _get_device_by_name(self, name: str) -> Device: - """Calls the ``get_devices()`` method and checks if the given device is available in the list. - - Args: - name: Takes device name as argument. - - Returns: - Device: - Returns device information as a Device object. - """ - for device in self._get_devices(): - if device.name == name: - return device - - def allow(self, device: Union[str, Device]) -> Union[Device, None]: - """Allows internet access to a device. - - Args: - device: Takes device name or Device object as an argument. - - Returns: - Device: - Returns the device object received from ``get_device_by_name()`` method. - """ - if isinstance(device, str): - tmp = device - LOGGER.info(f'Looking information on {device}') - if not (device := self._get_device_by_name(name=device)): - LOGGER.error(f'Device: {tmp} is not connected to {self.ssid}') - return - LOGGER.info(f'Granting internet access to {device.name}') - self.netgear.allow_block_device(mac_addr=device.mac, device_status='Allow') - return device - - def block(self, device: Union[str, Device]) -> Union[Device, None]: - """Blocks internet access to a device. - - Args: - device: Takes device name or Device object as an argument. - - Returns: - Device: - Returns the device object received from ``get_device_by_name()`` method. - """ - if isinstance(device, str): - tmp = device - LOGGER.info(f'Looking information on {device}') - if not (device := self._get_device_by_name(name=device)): - LOGGER.error(f'Device: {tmp} is not connected to {self.ssid}') - return - LOGGER.info(f'Blocking internet access to {device.name}') - self.netgear.allow_block_device(mac_addr=device.mac, device_status='Block') - return device - - def _dump_blocked(self, device: Device) -> NoReturn: - """Converts device object to a dictionary and dumps it into ``blocked.json`` file. - - Args: - device: Takes Device object as an argument. - """ - LOGGER.info(f'Details of {device.name} has been stored in {self.blocked}') - with open(self.blocked, 'a') as file: - # noinspection PyProtectedMember - dictionary = {time.time(): device._asdict()} - yaml.dump(dictionary, file, allow_unicode=True, default_flow_style=False, sort_keys=False) - - def _stasher(self, device: Device) -> NoReturn: - """Checks the ``blocked.json`` file for an existing record of the same device. - - If so, logs else calls ``dump_blocked()`` method. - - Args: - device: Takes Device object as an argument. - """ - blocked_devices = None - blocked = [] - if os.path.isfile(self.blocked): - with open(self.blocked) as file: - if file.read().strip(): - blocked_devices = yaml.load(stream=file, Loader=yaml.FullLoader) - if blocked_devices: - for epoch, device_info in blocked_devices.items(): - blocked.append(device_info.get('mac')) - - if device.mac not in blocked: - self._dump_blocked(device=device) - else: - LOGGER.info(f'{device.name} is a part of deny list.') - - def always_allow(self, device: Device or str) -> NoReturn: - """Allows internet access to a device. - - Saves the device name to ``snapshot.json`` to not block in future. - Removes the device name from ``blocked.json`` if an entry is present. - - Args: - device: Takes device name or Device object as an argument - """ - if isinstance(device, Device): - device = device.name # converts Device object to string - if not (device := self.allow(device=device)): # converts string to Device object - return - - with open(self.snapshot, 'r+') as file: - data = json.load(file) - file.seek(0) - if device.ip in list(data.keys()): - LOGGER.info(f'{device.name} is a part of allow list.') - data[device.ip][-1] = 'Allow' - LOGGER.info(f'Setting status to Allow for {device.name} in {self.snapshot}') - else: - data.update({str(device.ip): [str(device.name), str(device.type), str(device.allow_or_block)]}) - LOGGER.info(f'Adding {device.name} to {self.snapshot}') - - json.dump(data, file, indent=2) - file.truncate() - - if os.path.isfile(self.blocked): - with open(self.blocked, 'r+') as file: - if blocked_devices := yaml.load(stream=file, Loader=yaml.FullLoader): - for epoch, device_info in list(blocked_devices.items()): # convert to a list of dict - if device_info.get('mac') == device.mac: - LOGGER.info(f'Removing {device.name} from {self.blocked}') - del blocked_devices[epoch] - file.seek(0) - file.truncate() - if blocked_devices: - yaml.dump(blocked_devices, file, indent=2) - - def run(self, block: bool = False) -> NoReturn: - """Trigger to initiate a Network Scan and block the devices that are not present in ``snapshot.json`` file.""" - if not os.path.isfile(self.snapshot): - LOGGER.error(f'{self.snapshot} not found. Please run `LocalIPScan().create_snapshot()` and review it.') - raise FileNotFoundError( - f'{self.snapshot} is required' - ) - with open(self.snapshot) as file: - device_list = json.load(file) - threat = '' - for device in self._get_devices(): - if device.ip not in list(device_list.keys()): - LOGGER.warning(f'{device.name} with MAC address {device.mac} and a signal strength of {device.signal}% ' - f'has connected to {self.ssid}') - - if device.allow_or_block == 'Allow': - if block: - self.block(device=device) - self._stasher(device=device) - threat += f'\nName: {device.name}\nIP: {device.ip}\nMAC: {device.mac}' - else: - LOGGER.info(f'{device.name} does not have internet access.') - - if threat: - send_sms(msg=threat) + netgear.LocalIPScan().run() + elif module == models.SupportedModules.att: + if init: + att.create_snapshot() else: - LOGGER.info(f'NetScan has completed. No threats found on {self.ssid}') + att.run() + else: + raise ValueError( + "\n\nnetwork argument should either be '%s' or '%s'" % (models.SupportedModules.att, + models.SupportedModules.netgear) + ) if __name__ == '__main__': - if os.environ.get('DOCKER'): - logging.Formatter.converter = custom_time - LocalIPScan().run() + network_monitor(module=models.SupportedModules.att, init=False) diff --git a/doc_generator/_build/doctrees/README.doctree b/doc_generator/_build/doctrees/README.doctree deleted file mode 100644 index 1882541..0000000 Binary files a/doc_generator/_build/doctrees/README.doctree and /dev/null differ diff --git a/doc_generator/_build/doctrees/environment.pickle b/doc_generator/_build/doctrees/environment.pickle deleted file mode 100644 index 7888f72..0000000 Binary files a/doc_generator/_build/doctrees/environment.pickle and /dev/null differ diff --git a/doc_generator/_build/doctrees/index.doctree b/doc_generator/_build/doctrees/index.doctree deleted file mode 100644 index c408b2d..0000000 Binary files a/doc_generator/_build/doctrees/index.doctree and /dev/null differ diff --git a/doc_generator/_build/html/.buildinfo b/doc_generator/_build/html/.buildinfo deleted file mode 100644 index 229a098..0000000 --- a/doc_generator/_build/html/.buildinfo +++ /dev/null @@ -1,4 +0,0 @@ -# Sphinx build info version 1 -# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: fea9902a8e3e3ee2a1be87e8e7a27920 -tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/README.html b/docs/README.html index 3ff45b9..621cc50 100644 --- a/docs/README.html +++ b/docs/README.html @@ -1,10 +1,10 @@ - + - + NetScan — NetScan documentation @@ -13,6 +13,7 @@ + @@ -42,31 +43,50 @@

Navigation

-

NetScan

+

NetScan

Network Scanner to analyze devices connecting to the router and alert accordingly.

-

NetScan displays the connected devices, Wi-Fi signal info, speed, etc.

-

This app can display intruders IP addresses, MAC addresses, and lets the user Ping the device, and even Block user from -Wi-Fi.

+

This app can display intruders’ IP addresses, MAC addresses, and lets the user Ping the device, and even Block the device.

Block IP Address feature helps the user to remove the specified connections and block the specific IP address.

-
-

Docker Setup:

-
-

Commands:

-
    -
  • docker build --progress plain -t netscan .

  • -
  • docker run netscan

  • -
-

Additional options that can be added to the docker build command.

-
-
-

Reference

+
+

Blocking device feature is currently available only for Netgear router users.

+
+

+
+
+
+

Coding Standards

+

Docstring format: Google
+Styling conventions: PEP 8
+Clean code with pre-commit hooks: flake8 and +isort

-
-

Runbook

+
+

Release Notes

+

Requirement

+
python -m pip install changelog-generator
+
+
+

Usage

+
changelog reverse -f release_notes.rst -t 'Release Notes'
+
+
-
-

Repository

+
+

Linting

+

PreCommit will ensure linting, and the doc creation are run on every commit.

+

Requirement

+
pip install sphinx==5.1.1 pre-commit recommonmark
+
+
+

Usage

+
pre-commit run --all-files
+
+
+
+

Runbook

+

made-with-sphinx-doc

+

https://thevickypedia.github.io/netscan/

@@ -77,23 +97,24 @@

- +

@@ -133,7 +154,7 @@

Navigation

\ No newline at end of file diff --git a/docs/_sources/README.md.txt b/docs/_sources/README.md.txt index 57f15ed..2efca73 100644 --- a/docs/_sources/README.md.txt +++ b/docs/_sources/README.md.txt @@ -1,23 +1,48 @@ # NetScan Network Scanner to analyze devices connecting to the router and alert accordingly. -NetScan displays the connected devices, Wi-Fi signal info, speed, etc. - -This app can display intruders IP addresses, MAC addresses, and lets the user Ping the device, and even Block user from -Wi-Fi. +This app can display intruders' IP addresses, MAC addresses, and lets the user Ping the device, and even Block the device. Block IP Address feature helps the user to remove the specified connections and block the specific IP address. -### Docker Setup: +> Blocking device feature is currently available only for `Netgear` router users. + +```python + +``` + + +## Coding Standards +Docstring format: [`Google`](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
+Styling conventions: [`PEP 8`](https://www.python.org/dev/peps/pep-0008/)
+Clean code with pre-commit hooks: [`flake8`](https://flake8.pycqa.org/en/latest/) and +[`isort`](https://pycqa.github.io/isort/) + +## [Release Notes](https://github.com/thevickypedia/Jarvis/blob/master/release_notes.rst) +**Requirement** +```shell +python -m pip install changelog-generator +``` + +**Usage** +```shell +changelog reverse -f release_notes.rst -t 'Release Notes' +``` -###### Commands: -- `docker build --progress plain -t netscan .` -- `docker run netscan` +## Linting +`PreCommit` will ensure linting, and the doc creation are run on every commit. -[Additional options that can be added to the `docker build` command.](https://docs.docker.com/engine/reference/commandline/build/#options) +**Requirement** +```shell +pip install sphinx==5.1.1 pre-commit recommonmark +``` -###### [Reference](https://github.com/MatMaul/pynetgear) +**Usage** +```shell +pre-commit run --all-files +``` -###### [Runbook](https://thevickypedia.github.io/netscan/) +## Runbook +[![made-with-sphinx-doc](https://img.shields.io/badge/Code%20Docs-Sphinx-1f425f.svg)](https://www.sphinx-doc.org/en/master/man/sphinx-autogen.html) -###### [Repository](https://github.com/thevickypedia/netscan) +[https://thevickypedia.github.io/netscan/](https://thevickypedia.github.io/netscan/) diff --git a/docs/_sources/index.rst.txt b/docs/_sources/index.rst.txt index 85bed8e..d788cb7 100644 --- a/docs/_sources/index.rst.txt +++ b/docs/_sources/index.rst.txt @@ -19,6 +19,41 @@ NetScan :members: :undoc-members: +At&t +==== + +.. automodule:: modules.att + :members: + :undoc-members: + +Netgear +======= + +.. automodule:: modules.netgear + :members: + :undoc-members: + +Helper +====== + +.. automodule:: modules.helper + :members: + :undoc-members: + +Models +====== + +.. automodule:: modules.models + :members: + :undoc-members: + +Settings +======== + +.. automodule:: modules.settings + :members: + :undoc-members: + Indices and tables ================== diff --git a/docs/_static/_sphinx_javascript_frameworks_compat.js b/docs/_static/_sphinx_javascript_frameworks_compat.js new file mode 100644 index 0000000..8549469 --- /dev/null +++ b/docs/_static/_sphinx_javascript_frameworks_compat.js @@ -0,0 +1,134 @@ +/* + * _sphinx_javascript_frameworks_compat.js + * ~~~~~~~~~~ + * + * Compatability shim for jQuery and underscores.js. + * + * WILL BE REMOVED IN Sphinx 6.0 + * xref RemovedInSphinx60Warning + * + */ + +/** + * select a different prefix for underscore + */ +$u = _.noConflict(); + + +/** + * small helper function to urldecode strings + * + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL + */ +jQuery.urldecode = function(x) { + if (!x) { + return x + } + return decodeURIComponent(x.replace(/\+/g, ' ')); +}; + +/** + * small helper function to urlencode strings + */ +jQuery.urlencode = encodeURIComponent; + +/** + * This function returns the parsed url parameters of the + * current request. Multiple values per key are supported, + * it will always return arrays of strings for the value parts. + */ +jQuery.getQueryParameters = function(s) { + if (typeof s === 'undefined') + s = document.location.search; + var parts = s.substr(s.indexOf('?') + 1).split('&'); + var result = {}; + for (var i = 0; i < parts.length; i++) { + var tmp = parts[i].split('=', 2); + var key = jQuery.urldecode(tmp[0]); + var value = jQuery.urldecode(tmp[1]); + if (key in result) + result[key].push(value); + else + result[key] = [value]; + } + return result; +}; + +/** + * highlight a given string on a jquery object by wrapping it in + * span elements with the given class name. + */ +jQuery.fn.highlightText = function(text, className) { + function highlight(node, addItems) { + if (node.nodeType === 3) { + var val = node.nodeValue; + var pos = val.toLowerCase().indexOf(text); + if (pos >= 0 && + !jQuery(node.parentNode).hasClass(className) && + !jQuery(node.parentNode).hasClass("nohighlight")) { + var span; + var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.className = className; + } + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + node.parentNode.insertBefore(span, node.parentNode.insertBefore( + document.createTextNode(val.substr(pos + text.length)), + node.nextSibling)); + node.nodeValue = val.substr(0, pos); + if (isInSVG) { + var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); + var bbox = node.parentElement.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute('class', className); + addItems.push({ + "parent": node.parentNode, + "target": rect}); + } + } + } + else if (!jQuery(node).is("button, select, textarea")) { + jQuery.each(node.childNodes, function() { + highlight(this, addItems); + }); + } + } + var addItems = []; + var result = this.each(function() { + highlight(this, addItems); + }); + for (var i = 0; i < addItems.length; ++i) { + jQuery(addItems[i].parent).before(addItems[i].target); + } + return result; +}; + +/* + * backward compatibility for jQuery.browser + * This will be supported until firefox bug is fixed. + */ +if (!jQuery.browser) { + jQuery.uaMatch = function(ua) { + ua = ua.toLowerCase(); + + var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || + /(webkit)[ \/]([\w.]+)/.exec(ua) || + /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || + /(msie) ([\w.]+)/.exec(ua) || + ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || + []; + + return { + browser: match[ 1 ] || "", + version: match[ 2 ] || "0" + }; + }; + jQuery.browser = {}; + jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; +} diff --git a/docs/_static/basic.css b/docs/_static/basic.css index 4b155a1..bdfb3ef 100644 --- a/docs/_static/basic.css +++ b/docs/_static/basic.css @@ -4,7 +4,7 @@ * * Sphinx stylesheet -- basic theme. * - * :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. + * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ @@ -222,7 +222,7 @@ table.modindextable td { /* -- general body styles --------------------------------------------------- */ div.body { - min-width: 450px; + min-width: 360px; max-width: 80%; } @@ -237,16 +237,6 @@ a.headerlink { visibility: hidden; } -a.brackets:before, -span.brackets > a:before{ - content: "["; -} - -a.brackets:after, -span.brackets > a:after { - content: "]"; -} - h1:hover > a.headerlink, h2:hover > a.headerlink, h3:hover > a.headerlink, @@ -334,13 +324,15 @@ aside.sidebar { p.sidebar-title { font-weight: bold; } - +nav.contents, +aside.topic, div.admonition, div.topic, blockquote { clear: left; } /* -- topics ---------------------------------------------------------------- */ - +nav.contents, +aside.topic, div.topic { border: 1px solid #ccc; padding: 7px; @@ -379,6 +371,8 @@ div.body p.centered { div.sidebar > :last-child, aside.sidebar > :last-child, +nav.contents > :last-child, +aside.topic > :last-child, div.topic > :last-child, div.admonition > :last-child { margin-bottom: 0; @@ -386,6 +380,8 @@ div.admonition > :last-child { div.sidebar::after, aside.sidebar::after, +nav.contents::after, +aside.topic::after, div.topic::after, div.admonition::after, blockquote::after { @@ -428,10 +424,6 @@ table.docutils td, table.docutils th { border-bottom: 1px solid #aaa; } -table.footnote td, table.footnote th { - border: 0 !important; -} - th { text-align: left; padding-right: 5px; @@ -614,20 +606,26 @@ ol.simple p, ul.simple p { margin-bottom: 0; } - -dl.footnote > dt, -dl.citation > dt { +aside.footnote > span, +div.citation > span { float: left; - margin-right: 0.5em; } - -dl.footnote > dd, -dl.citation > dd { +aside.footnote > span:last-of-type, +div.citation > span:last-of-type { + padding-right: 0.5em; +} +aside.footnote > p { + margin-left: 2em; +} +div.citation > p { + margin-left: 4em; +} +aside.footnote > p:last-of-type, +div.citation > p:last-of-type { margin-bottom: 0em; } - -dl.footnote > dd:after, -dl.citation > dd:after { +aside.footnote > p:last-of-type:after, +div.citation > p:last-of-type:after { content: ""; clear: both; } @@ -644,10 +642,6 @@ dl.field-list > dt { padding-right: 5px; } -dl.field-list > dt:after { - content: ":"; -} - dl.field-list > dd { padding-left: 0.5em; margin-top: 0em; @@ -731,8 +725,9 @@ dl.glossary dt { .classifier:before { font-style: normal; - margin: 0.5em; + margin: 0 0.5em; content: ":"; + display: inline-block; } abbr, acronym { @@ -756,6 +751,7 @@ span.pre { -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; + white-space: nowrap; } div[class*="highlight-"] { diff --git a/docs/_static/classic.css b/docs/_static/classic.css index dcae946..92cac9f 100644 --- a/docs/_static/classic.css +++ b/docs/_static/classic.css @@ -4,7 +4,7 @@ * * Sphinx stylesheet -- classic theme. * - * :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. + * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ @@ -28,6 +28,7 @@ body { } div.document { + display: flex; background-color: #1c4e63; } @@ -204,6 +205,8 @@ div.seealso { background-color: #ffc; border: 1px solid #ff6; } +nav.contents, +aside.topic, div.topic { background-color: #eee; diff --git a/docs/_static/doctools.js b/docs/_static/doctools.js index 8cbf1b1..c3db08d 100644 --- a/docs/_static/doctools.js +++ b/docs/_static/doctools.js @@ -2,322 +2,263 @@ * doctools.js * ~~~~~~~~~~~ * - * Sphinx JavaScript utilities for all documentation. + * Base JavaScript utilities for all Sphinx HTML documentation. * - * :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. + * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ +"use strict"; -/** - * select a different prefix for underscore - */ -$u = _.noConflict(); - -/** - * make the code below compatible with browsers without - * an installed firebug like debugger -if (!window.console || !console.firebug) { - var names = ["log", "debug", "info", "warn", "error", "assert", "dir", - "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", - "profile", "profileEnd"]; - window.console = {}; - for (var i = 0; i < names.length; ++i) - window.console[names[i]] = function() {}; -} - */ - -/** - * small helper function to urldecode strings - * - * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL - */ -jQuery.urldecode = function(x) { - if (!x) { - return x +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); } - return decodeURIComponent(x.replace(/\+/g, ' ')); }; /** - * small helper function to urlencode strings + * highlight a given string on a node by wrapping it in + * span elements with the given class name. */ -jQuery.urlencode = encodeURIComponent; +const _highlight = (node, addItems, text, className) => { + if (node.nodeType === Node.TEXT_NODE) { + const val = node.nodeValue; + const parent = node.parentNode; + const pos = val.toLowerCase().indexOf(text); + if ( + pos >= 0 && + !parent.classList.contains(className) && + !parent.classList.contains("nohighlight") + ) { + let span; -/** - * This function returns the parsed url parameters of the - * current request. Multiple values per key are supported, - * it will always return arrays of strings for the value parts. - */ -jQuery.getQueryParameters = function(s) { - if (typeof s === 'undefined') - s = document.location.search; - var parts = s.substr(s.indexOf('?') + 1).split('&'); - var result = {}; - for (var i = 0; i < parts.length; i++) { - var tmp = parts[i].split('=', 2); - var key = jQuery.urldecode(tmp[0]); - var value = jQuery.urldecode(tmp[1]); - if (key in result) - result[key].push(value); - else - result[key] = [value]; - } - return result; -}; + const closestNode = parent.closest("body, svg, foreignObject"); + const isInSVG = closestNode && closestNode.matches("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.classList.add(className); + } -/** - * highlight a given string on a jquery object by wrapping it in - * span elements with the given class name. - */ -jQuery.fn.highlightText = function(text, className) { - function highlight(node, addItems) { - if (node.nodeType === 3) { - var val = node.nodeValue; - var pos = val.toLowerCase().indexOf(text); - if (pos >= 0 && - !jQuery(node.parentNode).hasClass(className) && - !jQuery(node.parentNode).hasClass("nohighlight")) { - var span; - var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); - if (isInSVG) { - span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); - } else { - span = document.createElement("span"); - span.className = className; - } - span.appendChild(document.createTextNode(val.substr(pos, text.length))); - node.parentNode.insertBefore(span, node.parentNode.insertBefore( + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + parent.insertBefore( + span, + parent.insertBefore( document.createTextNode(val.substr(pos + text.length)), - node.nextSibling)); - node.nodeValue = val.substr(0, pos); - if (isInSVG) { - var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); - var bbox = node.parentElement.getBBox(); - rect.x.baseVal.value = bbox.x; - rect.y.baseVal.value = bbox.y; - rect.width.baseVal.value = bbox.width; - rect.height.baseVal.value = bbox.height; - rect.setAttribute('class', className); - addItems.push({ - "parent": node.parentNode, - "target": rect}); - } + node.nextSibling + ) + ); + node.nodeValue = val.substr(0, pos); + + if (isInSVG) { + const rect = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect" + ); + const bbox = parent.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute("class", className); + addItems.push({ parent: parent, target: rect }); } } - else if (!jQuery(node).is("button, select, textarea")) { - jQuery.each(node.childNodes, function() { - highlight(this, addItems); - }); - } + } else if (node.matches && !node.matches("button, select, textarea")) { + node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); } - var addItems = []; - var result = this.each(function() { - highlight(this, addItems); - }); - for (var i = 0; i < addItems.length; ++i) { - jQuery(addItems[i].parent).before(addItems[i].target); - } - return result; }; - -/* - * backward compatibility for jQuery.browser - * This will be supported until firefox bug is fixed. - */ -if (!jQuery.browser) { - jQuery.uaMatch = function(ua) { - ua = ua.toLowerCase(); - - var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || - /(webkit)[ \/]([\w.]+)/.exec(ua) || - /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || - /(msie) ([\w.]+)/.exec(ua) || - ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || - []; - - return { - browser: match[ 1 ] || "", - version: match[ 2 ] || "0" - }; - }; - jQuery.browser = {}; - jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; -} +const _highlightText = (thisNode, text, className) => { + let addItems = []; + _highlight(thisNode, addItems, text, className); + addItems.forEach((obj) => + obj.parent.insertAdjacentElement("beforebegin", obj.target) + ); +}; /** * Small JavaScript module for the documentation. */ -var Documentation = { - - init : function() { - this.fixFirefoxAnchorBug(); - this.highlightSearchWords(); - this.initIndexTable(); - if (DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) { - this.initOnKeyListeners(); - } +const Documentation = { + init: () => { + Documentation.highlightSearchWords(); + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); }, /** * i18n support */ - TRANSLATIONS : {}, - PLURAL_EXPR : function(n) { return n === 1 ? 0 : 1; }, - LOCALE : 'unknown', + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", // gettext and ngettext don't access this so that the functions // can safely bound to a different name (_ = Documentation.gettext) - gettext : function(string) { - var translated = Documentation.TRANSLATIONS[string]; - if (typeof translated === 'undefined') - return string; - return (typeof translated === 'string') ? translated : translated[0]; - }, - - ngettext : function(singular, plural, n) { - var translated = Documentation.TRANSLATIONS[singular]; - if (typeof translated === 'undefined') - return (n == 1) ? singular : plural; - return translated[Documentation.PLURALEXPR(n)]; - }, - - addTranslations : function(catalog) { - for (var key in catalog.messages) - this.TRANSLATIONS[key] = catalog.messages[key]; - this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')'); - this.LOCALE = catalog.locale; + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } }, - /** - * add context elements like header anchor links - */ - addContextElements : function() { - $('div[id] > :header:first').each(function() { - $('\u00B6'). - attr('href', '#' + this.id). - attr('title', _('Permalink to this headline')). - appendTo(this); - }); - $('dt[id]').each(function() { - $('\u00B6'). - attr('href', '#' + this.id). - attr('title', _('Permalink to this definition')). - appendTo(this); - }); + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; }, - /** - * workaround a firefox stupidity - * see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075 - */ - fixFirefoxAnchorBug : function() { - if (document.location.hash && $.browser.mozilla) - window.setTimeout(function() { - document.location.href += ''; - }, 10); + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})` + ); + Documentation.LOCALE = catalog.locale; }, /** * highlight the search words provided in the url in the text */ - highlightSearchWords : function() { - var params = $.getQueryParameters(); - var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : []; - if (terms.length) { - var body = $('div.body'); - if (!body.length) { - body = $('body'); - } - window.setTimeout(function() { - $.each(terms, function() { - body.highlightText(this.toLowerCase(), 'highlighted'); - }); - }, 10); - $('') - .appendTo($('#searchbox')); - } - }, + highlightSearchWords: () => { + const highlight = + new URLSearchParams(window.location.search).get("highlight") || ""; + const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); + if (terms.length === 0) return; // nothing to do - /** - * init the domain index toggle buttons - */ - initIndexTable : function() { - var togglers = $('img.toggler').click(function() { - var src = $(this).attr('src'); - var idnum = $(this).attr('id').substr(7); - $('tr.cg-' + idnum).toggle(); - if (src.substr(-9) === 'minus.png') - $(this).attr('src', src.substr(0, src.length-9) + 'plus.png'); - else - $(this).attr('src', src.substr(0, src.length-8) + 'minus.png'); - }).css('display', ''); - if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) { - togglers.click(); - } + // There should never be more than one element matching "div.body" + const divBody = document.querySelectorAll("div.body"); + const body = divBody.length ? divBody[0] : document.querySelector("body"); + window.setTimeout(() => { + terms.forEach((term) => _highlightText(body, term, "highlighted")); + }, 10); + + const searchBox = document.getElementById("searchbox"); + if (searchBox === null) return; + searchBox.appendChild( + document + .createRange() + .createContextualFragment( + '" + ) + ); }, /** * helper function to hide the search marks again */ - hideSearchWords : function() { - $('#searchbox .highlight-link').fadeOut(300); - $('span.highlighted').removeClass('highlighted'); + hideSearchWords: () => { + document + .querySelectorAll("#searchbox .highlight-link") + .forEach((el) => el.remove()); + document + .querySelectorAll("span.highlighted") + .forEach((el) => el.classList.remove("highlighted")); + const url = new URL(window.location); + url.searchParams.delete("highlight"); + window.history.replaceState({}, "", url); }, /** - * make the url absolute + * helper function to focus on search bar */ - makeURL : function(relativeURL) { - return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL; + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); }, /** - * get the current relative url + * Initialise the domain index toggle buttons */ - getCurrentURL : function() { - var path = document.location.pathname; - var parts = path.split(/\//); - $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() { - if (this === '..') - parts.pop(); - }); - var url = parts.join('/'); - return path.substring(url.lastIndexOf('/') + 1, path.length - 1); + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)) + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); }, - initOnKeyListeners: function() { - $(document).keydown(function(event) { - var activeElementType = document.activeElement.tagName; - // don't navigate when in search box, textarea, dropdown or button - if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT' - && activeElementType !== 'BUTTON' && !event.altKey && !event.ctrlKey && !event.metaKey - && !event.shiftKey) { - switch (event.keyCode) { - case 37: // left - var prevHref = $('link[rel="prev"]').prop('href'); - if (prevHref) { - window.location.href = prevHref; - return false; + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && + !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + const blacklistedElements = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", + ]); + document.addEventListener("keydown", (event) => { + if (blacklistedElements.has(document.activeElement.tagName)) return; // bail for input elements + if (event.altKey || event.ctrlKey || event.metaKey) return; // bail with special keys + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); } break; - case 39: // right - var nextHref = $('link[rel="next"]').prop('href'); - if (nextHref) { - window.location.href = nextHref; - return false; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); } break; + case "Escape": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.hideSearchWords(); + event.preventDefault(); } } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } }); - } + }, }; // quick alias for translations -_ = Documentation.gettext; +const _ = Documentation.gettext; -$(document).ready(function() { - Documentation.init(); -}); +_ready(Documentation.init); diff --git a/docs/_static/documentation_options.js b/docs/_static/documentation_options.js index 2fa8c97..b57ae3b 100644 --- a/docs/_static/documentation_options.js +++ b/docs/_static/documentation_options.js @@ -1,12 +1,14 @@ var DOCUMENTATION_OPTIONS = { URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), VERSION: '', - LANGUAGE: 'None', + LANGUAGE: 'en', COLLAPSE_INDEX: false, BUILDER: 'html', FILE_SUFFIX: '.html', LINK_SUFFIX: '.html', HAS_SOURCE: true, SOURCELINK_SUFFIX: '.txt', - NAVIGATION_WITH_KEYS: false + NAVIGATION_WITH_KEYS: false, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, }; \ No newline at end of file diff --git a/docs/_static/jquery-3.5.1.js b/docs/_static/jquery-3.6.0.js similarity index 98% rename from docs/_static/jquery-3.5.1.js rename to docs/_static/jquery-3.6.0.js index 5093733..fc6c299 100644 --- a/docs/_static/jquery-3.5.1.js +++ b/docs/_static/jquery-3.6.0.js @@ -1,15 +1,15 @@ /*! - * jQuery JavaScript Library v3.5.1 + * jQuery JavaScript Library v3.6.0 * https://jquery.com/ * * Includes Sizzle.js * https://sizzlejs.com/ * - * Copyright JS Foundation and other contributors + * Copyright OpenJS Foundation and other contributors * Released under the MIT license * https://jquery.org/license * - * Date: 2020-05-04T22:49Z + * Date: 2021-03-02T17:08Z */ ( function( global, factory ) { @@ -76,12 +76,16 @@ var support = {}; var isFunction = function isFunction( obj ) { - // Support: Chrome <=57, Firefox <=52 - // In some browsers, typeof returns "function" for HTML elements - // (i.e., `typeof document.createElement( "object" ) === "function"`). - // We don't want to classify *any* DOM node as a function. - return typeof obj === "function" && typeof obj.nodeType !== "number"; - }; + // Support: Chrome <=57, Firefox <=52 + // In some browsers, typeof returns "function" for HTML elements + // (i.e., `typeof document.createElement( "object" ) === "function"`). + // We don't want to classify *any* DOM node as a function. + // Support: QtWeb <=3.8.5, WebKit <=534.34, wkhtmltopdf tool <=0.12.5 + // Plus for old WebKit, typeof returns "function" for HTML collections + // (e.g., `typeof document.getElementsByTagName("div") === "function"`). (gh-4756) + return typeof obj === "function" && typeof obj.nodeType !== "number" && + typeof obj.item !== "function"; + }; var isWindow = function isWindow( obj ) { @@ -147,7 +151,7 @@ function toType( obj ) { var - version = "3.5.1", + version = "3.6.0", // Define a local copy of jQuery jQuery = function( selector, context ) { @@ -401,7 +405,7 @@ jQuery.extend( { if ( isArrayLike( Object( arr ) ) ) { jQuery.merge( ret, typeof arr === "string" ? - [ arr ] : arr + [ arr ] : arr ); } else { push.call( ret, arr ); @@ -496,9 +500,9 @@ if ( typeof Symbol === "function" ) { // Populate the class2type map jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), -function( _i, name ) { - class2type[ "[object " + name + "]" ] = name.toLowerCase(); -} ); + function( _i, name ) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); + } ); function isArrayLike( obj ) { @@ -518,14 +522,14 @@ function isArrayLike( obj ) { } var Sizzle = /*! - * Sizzle CSS Selector Engine v2.3.5 + * Sizzle CSS Selector Engine v2.3.6 * https://sizzlejs.com/ * * Copyright JS Foundation and other contributors * Released under the MIT license * https://js.foundation/ * - * Date: 2020-03-14 + * Date: 2021-02-16 */ ( function( window ) { var i, @@ -1108,8 +1112,8 @@ support = Sizzle.support = {}; * @returns {Boolean} True iff elem is a non-HTML XML node */ isXML = Sizzle.isXML = function( elem ) { - var namespace = elem.namespaceURI, - docElem = ( elem.ownerDocument || elem ).documentElement; + var namespace = elem && elem.namespaceURI, + docElem = elem && ( elem.ownerDocument || elem ).documentElement; // Support: IE <=8 // Assume HTML when documentElement doesn't yet exist, such as inside loading iframes @@ -3024,9 +3028,9 @@ var rneedsContext = jQuery.expr.match.needsContext; function nodeName( elem, name ) { - return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); + return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); -}; +} var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i ); @@ -3997,8 +4001,8 @@ jQuery.extend( { resolveContexts = Array( i ), resolveValues = slice.call( arguments ), - // the master Deferred - master = jQuery.Deferred(), + // the primary Deferred + primary = jQuery.Deferred(), // subordinate callback factory updateFunc = function( i ) { @@ -4006,30 +4010,30 @@ jQuery.extend( { resolveContexts[ i ] = this; resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; if ( !( --remaining ) ) { - master.resolveWith( resolveContexts, resolveValues ); + primary.resolveWith( resolveContexts, resolveValues ); } }; }; // Single- and empty arguments are adopted like Promise.resolve if ( remaining <= 1 ) { - adoptValue( singleValue, master.done( updateFunc( i ) ).resolve, master.reject, + adoptValue( singleValue, primary.done( updateFunc( i ) ).resolve, primary.reject, !remaining ); // Use .then() to unwrap secondary thenables (cf. gh-3000) - if ( master.state() === "pending" || + if ( primary.state() === "pending" || isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) { - return master.then(); + return primary.then(); } } // Multiple arguments are aggregated like Promise.all array elements while ( i-- ) { - adoptValue( resolveValues[ i ], updateFunc( i ), master.reject ); + adoptValue( resolveValues[ i ], updateFunc( i ), primary.reject ); } - return master.promise(); + return primary.promise(); } } ); @@ -4180,8 +4184,8 @@ var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { for ( ; i < len; i++ ) { fn( elems[ i ], key, raw ? - value : - value.call( elems[ i ], i, fn( elems[ i ], key ) ) + value : + value.call( elems[ i ], i, fn( elems[ i ], key ) ) ); } } @@ -5089,10 +5093,7 @@ function buildFragment( elems, context, scripts, selection, ignored ) { } -var - rkeyEvent = /^key/, - rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/, - rtypenamespace = /^([^.]*)(?:\.(.+)|)/; +var rtypenamespace = /^([^.]*)(?:\.(.+)|)/; function returnTrue() { return true; @@ -5387,8 +5388,8 @@ jQuery.event = { event = jQuery.event.fix( nativeEvent ), handlers = ( - dataPriv.get( this, "events" ) || Object.create( null ) - )[ event.type ] || [], + dataPriv.get( this, "events" ) || Object.create( null ) + )[ event.type ] || [], special = jQuery.event.special[ event.type ] || {}; // Use the fix-ed jQuery.Event rather than the (read-only) native event @@ -5512,12 +5513,12 @@ jQuery.event = { get: isFunction( hook ) ? function() { if ( this.originalEvent ) { - return hook( this.originalEvent ); + return hook( this.originalEvent ); } } : function() { if ( this.originalEvent ) { - return this.originalEvent[ name ]; + return this.originalEvent[ name ]; } }, @@ -5656,7 +5657,13 @@ function leverageNative( el, type, expectSync ) { // Cancel the outer synthetic event event.stopImmediatePropagation(); event.preventDefault(); - return result.value; + + // Support: Chrome 86+ + // In Chrome, if an element having a focusout handler is blurred by + // clicking outside of it, it invokes the handler synchronously. If + // that handler calls `.remove()` on the element, the data is cleared, + // leaving `result` undefined. We need to guard against this. + return result && result.value; } // If this is an inner synthetic event for an event with a bubbling surrogate @@ -5821,34 +5828,7 @@ jQuery.each( { targetTouches: true, toElement: true, touches: true, - - which: function( event ) { - var button = event.button; - - // Add which for key events - if ( event.which == null && rkeyEvent.test( event.type ) ) { - return event.charCode != null ? event.charCode : event.keyCode; - } - - // Add which for click: 1 === left; 2 === middle; 3 === right - if ( !event.which && button !== undefined && rmouseEvent.test( event.type ) ) { - if ( button & 1 ) { - return 1; - } - - if ( button & 2 ) { - return 3; - } - - if ( button & 4 ) { - return 2; - } - - return 0; - } - - return event.which; - } + which: true }, jQuery.event.addProp ); jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) { @@ -5874,6 +5854,12 @@ jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateTyp return true; }, + // Suppress native focus or blur as it's already being fired + // in leverageNative. + _default: function() { + return true; + }, + delegateType: delegateType }; } ); @@ -6541,6 +6527,10 @@ var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" ); // set in CSS while `offset*` properties report correct values. // Behavior in IE 9 is more subtle than in newer versions & it passes // some versions of this test; make sure not to make it pass there! + // + // Support: Firefox 70+ + // Only Firefox includes border widths + // in computed dimensions. (gh-4529) reliableTrDimensions: function() { var table, tr, trChild, trStyle; if ( reliableTrDimensionsVal == null ) { @@ -6548,17 +6538,32 @@ var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" ); tr = document.createElement( "tr" ); trChild = document.createElement( "div" ); - table.style.cssText = "position:absolute;left:-11111px"; + table.style.cssText = "position:absolute;left:-11111px;border-collapse:separate"; + tr.style.cssText = "border:1px solid"; + + // Support: Chrome 86+ + // Height set through cssText does not get applied. + // Computed height then comes back as 0. tr.style.height = "1px"; trChild.style.height = "9px"; + // Support: Android 8 Chrome 86+ + // In our bodyBackground.html iframe, + // display for all div elements is set to "inline", + // which causes a problem only in Android 8 Chrome 86. + // Ensuring the div is display: block + // gets around this issue. + trChild.style.display = "block"; + documentElement .appendChild( table ) .appendChild( tr ) .appendChild( trChild ); trStyle = window.getComputedStyle( tr ); - reliableTrDimensionsVal = parseInt( trStyle.height ) > 3; + reliableTrDimensionsVal = ( parseInt( trStyle.height, 10 ) + + parseInt( trStyle.borderTopWidth, 10 ) + + parseInt( trStyle.borderBottomWidth, 10 ) ) === tr.offsetHeight; documentElement.removeChild( table ); } @@ -7022,10 +7027,10 @@ jQuery.each( [ "height", "width" ], function( _i, dimension ) { // Running getBoundingClientRect on a disconnected node // in IE throws an error. ( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ? - swap( elem, cssShow, function() { - return getWidthOrHeight( elem, dimension, extra ); - } ) : - getWidthOrHeight( elem, dimension, extra ); + swap( elem, cssShow, function() { + return getWidthOrHeight( elem, dimension, extra ); + } ) : + getWidthOrHeight( elem, dimension, extra ); } }, @@ -7084,7 +7089,7 @@ jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft, swap( elem, { marginLeft: 0 }, function() { return elem.getBoundingClientRect().left; } ) - ) + "px"; + ) + "px"; } } ); @@ -7223,7 +7228,7 @@ Tween.propHooks = { if ( jQuery.fx.step[ tween.prop ] ) { jQuery.fx.step[ tween.prop ]( tween ); } else if ( tween.elem.nodeType === 1 && ( - jQuery.cssHooks[ tween.prop ] || + jQuery.cssHooks[ tween.prop ] || tween.elem.style[ finalPropName( tween.prop ) ] != null ) ) { jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); } else { @@ -7468,7 +7473,7 @@ function defaultPrefilter( elem, props, opts ) { anim.done( function() { - /* eslint-enable no-loop-func */ + /* eslint-enable no-loop-func */ // The final step of a "hide" animation is actually hiding the element if ( !hidden ) { @@ -7588,7 +7593,7 @@ function Animation( elem, properties, options ) { tweens: [], createTween: function( prop, end ) { var tween = jQuery.Tween( elem, animation.opts, prop, end, - animation.opts.specialEasing[ prop ] || animation.opts.easing ); + animation.opts.specialEasing[ prop ] || animation.opts.easing ); animation.tweens.push( tween ); return tween; }, @@ -7761,7 +7766,8 @@ jQuery.fn.extend( { anim.stop( true ); } }; - doAnimation.finish = doAnimation; + + doAnimation.finish = doAnimation; return empty || optall.queue === false ? this.each( doAnimation ) : @@ -8401,8 +8407,8 @@ jQuery.fn.extend( { if ( this.setAttribute ) { this.setAttribute( "class", className || value === false ? - "" : - dataPriv.get( this, "__className__" ) || "" + "" : + dataPriv.get( this, "__className__" ) || "" ); } } @@ -8417,7 +8423,7 @@ jQuery.fn.extend( { while ( ( elem = this[ i++ ] ) ) { if ( elem.nodeType === 1 && ( " " + stripAndCollapse( getClass( elem ) ) + " " ).indexOf( className ) > -1 ) { - return true; + return true; } } @@ -8707,9 +8713,7 @@ jQuery.extend( jQuery.event, { special.bindType || type; // jQuery handler - handle = ( - dataPriv.get( cur, "events" ) || Object.create( null ) - )[ event.type ] && + handle = ( dataPriv.get( cur, "events" ) || Object.create( null ) )[ event.type ] && dataPriv.get( cur, "handle" ); if ( handle ) { handle.apply( cur, data ); @@ -8856,7 +8860,7 @@ var rquery = ( /\?/ ); // Cross-browser xml parsing jQuery.parseXML = function( data ) { - var xml; + var xml, parserErrorElem; if ( !data || typeof data !== "string" ) { return null; } @@ -8865,12 +8869,17 @@ jQuery.parseXML = function( data ) { // IE throws on parseFromString with invalid input. try { xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" ); - } catch ( e ) { - xml = undefined; - } + } catch ( e ) {} - if ( !xml || xml.getElementsByTagName( "parsererror" ).length ) { - jQuery.error( "Invalid XML: " + data ); + parserErrorElem = xml && xml.getElementsByTagName( "parsererror" )[ 0 ]; + if ( !xml || parserErrorElem ) { + jQuery.error( "Invalid XML: " + ( + parserErrorElem ? + jQuery.map( parserErrorElem.childNodes, function( el ) { + return el.textContent; + } ).join( "\n" ) : + data + ) ); } return xml; }; @@ -8971,16 +8980,14 @@ jQuery.fn.extend( { // Can add propHook for "elements" to filter or add form elements var elements = jQuery.prop( this, "elements" ); return elements ? jQuery.makeArray( elements ) : this; - } ) - .filter( function() { + } ).filter( function() { var type = this.type; // Use .is( ":disabled" ) so that fieldset[disabled] works return this.name && !jQuery( this ).is( ":disabled" ) && rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) && ( this.checked || !rcheckableType.test( type ) ); - } ) - .map( function( _i, elem ) { + } ).map( function( _i, elem ) { var val = jQuery( this ).val(); if ( val == null ) { @@ -9033,7 +9040,8 @@ var // Anchor tag for parsing the document origin originAnchor = document.createElement( "a" ); - originAnchor.href = location.href; + +originAnchor.href = location.href; // Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport function addToPrefiltersOrTransports( structure ) { @@ -9414,8 +9422,8 @@ jQuery.extend( { // Context for global events is callbackContext if it is a DOM node or jQuery collection globalEventContext = s.context && ( callbackContext.nodeType || callbackContext.jquery ) ? - jQuery( callbackContext ) : - jQuery.event, + jQuery( callbackContext ) : + jQuery.event, // Deferreds deferred = jQuery.Deferred(), @@ -9727,8 +9735,10 @@ jQuery.extend( { response = ajaxHandleResponses( s, jqXHR, responses ); } - // Use a noop converter for missing script - if ( !isSuccess && jQuery.inArray( "script", s.dataTypes ) > -1 ) { + // Use a noop converter for missing script but not if jsonp + if ( !isSuccess && + jQuery.inArray( "script", s.dataTypes ) > -1 && + jQuery.inArray( "json", s.dataTypes ) < 0 ) { s.converters[ "text script" ] = function() {}; } @@ -10466,12 +10476,6 @@ jQuery.offset = { options.using.call( elem, props ); } else { - if ( typeof props.top === "number" ) { - props.top += "px"; - } - if ( typeof props.left === "number" ) { - props.left += "px"; - } curElem.css( props ); } } @@ -10640,8 +10644,11 @@ jQuery.each( [ "top", "left" ], function( _i, prop ) { // Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods jQuery.each( { Height: "height", Width: "width" }, function( name, type ) { - jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name }, - function( defaultExtra, funcName ) { + jQuery.each( { + padding: "inner" + name, + content: type, + "": "outer" + name + }, function( defaultExtra, funcName ) { // Margin is only for outerHeight, outerWidth jQuery.fn[ funcName ] = function( margin, value ) { @@ -10726,7 +10733,8 @@ jQuery.fn.extend( { } } ); -jQuery.each( ( "blur focus focusin focusout resize scroll click dblclick " + +jQuery.each( + ( "blur focus focusin focusout resize scroll click dblclick " + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + "change select submit keydown keypress keyup contextmenu" ).split( " " ), function( _i, name ) { @@ -10737,7 +10745,8 @@ jQuery.each( ( "blur focus focusin focusout resize scroll click dblclick " + this.on( name, null, data, fn ) : this.trigger( name ); }; - } ); + } +); diff --git a/docs/_static/jquery.js b/docs/_static/jquery.js index b061403..c4c6022 100644 --- a/docs/_static/jquery.js +++ b/docs/_static/jquery.js @@ -1,2 +1,2 @@ -/*! jQuery v3.5.1 | (c) JS Foundation and other contributors | jquery.org/license */ -!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.5.1",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function D(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||j,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,j=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function qe(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function Le(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function He(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Oe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||S.expando+"_"+Ct.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(Et.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((Ut=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Ut.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):("number"==typeof f.top&&(f.top+="px"),"number"==typeof f.left&&(f.left+="px"),c.css(f))}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=$e(y.pixelPosition,function(e,t){if(t)return t=Be(e,n),Me.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0 { + const [docname, title, anchor, descr, score, filename] = result + return score }, */ @@ -28,9 +30,11 @@ if (!Scorer) { // or matches in the last dotted part of the object name objPartialMatch: 6, // Additive scores depending on the priority of the object - objPrio: {0: 15, // used to be importantResults - 1: 5, // used to be objectResults - 2: -5}, // used to be unimportantResults + objPrio: { + 0: 15, // used to be importantResults + 1: 5, // used to be objectResults + 2: -5, // used to be unimportantResults + }, // Used when the priority is not in the mapping. objPrioDefault: 0, @@ -39,452 +43,453 @@ if (!Scorer) { partialTitle: 7, // query found in terms term: 5, - partialTerm: 2 + partialTerm: 2, }; } -if (!splitQuery) { - function splitQuery(query) { - return query.split(/\s+/); +const _removeChildren = (element) => { + while (element && element.lastChild) element.removeChild(element.lastChild); +}; + +/** + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping + */ +const _escapeRegExp = (string) => + string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string + +const _displayItem = (item, highlightTerms, searchTerms) => { + const docBuilder = DOCUMENTATION_OPTIONS.BUILDER; + const docUrlRoot = DOCUMENTATION_OPTIONS.URL_ROOT; + const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX; + const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX; + const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY; + + const [docName, title, anchor, descr] = item; + + let listItem = document.createElement("li"); + let requestUrl; + let linkUrl; + if (docBuilder === "dirhtml") { + // dirhtml builder + let dirname = docName + "/"; + if (dirname.match(/\/index\/$/)) + dirname = dirname.substring(0, dirname.length - 6); + else if (dirname === "index/") dirname = ""; + requestUrl = docUrlRoot + dirname; + linkUrl = requestUrl; + } else { + // normal html builders + requestUrl = docUrlRoot + docName + docFileSuffix; + linkUrl = docName + docLinkSuffix; + } + const params = new URLSearchParams(); + params.set("highlight", [...highlightTerms].join(" ")); + let linkEl = listItem.appendChild(document.createElement("a")); + linkEl.href = linkUrl + "?" + params.toString() + anchor; + linkEl.innerHTML = title; + if (descr) + listItem.appendChild(document.createElement("span")).innerHTML = + " (" + descr + ")"; + else if (showSearchSummary) + fetch(requestUrl) + .then((responseData) => responseData.text()) + .then((data) => { + if (data) + listItem.appendChild( + Search.makeSearchSummary(data, searchTerms, highlightTerms) + ); + }); + Search.output.appendChild(listItem); +}; +const _finishSearch = (resultCount) => { + Search.stopPulse(); + Search.title.innerText = _("Search Results"); + if (!resultCount) + Search.status.innerText = Documentation.gettext( + "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories." + ); + else + Search.status.innerText = _( + `Search finished, found ${resultCount} page(s) matching the search query.` + ); +}; +const _displayNextItem = ( + results, + resultCount, + highlightTerms, + searchTerms +) => { + // results left, load the summary and display it + // this is intended to be dynamic (don't sub resultsCount) + if (results.length) { + _displayItem(results.pop(), highlightTerms, searchTerms); + setTimeout( + () => _displayNextItem(results, resultCount, highlightTerms, searchTerms), + 5 + ); } + // search finished, update title and status message + else _finishSearch(resultCount); +}; + +/** + * Default splitQuery function. Can be overridden in ``sphinx.search`` with a + * custom function per language. + * + * The regular expression works by splitting the string on consecutive characters + * that are not Unicode letters, numbers, underscores, or emoji characters. + * This is the same as ``\W+`` in Python, preserving the surrogate pair area. + */ +if (typeof splitQuery === "undefined") { + var splitQuery = (query) => query + .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu) + .filter(term => term) // remove remaining empty strings } /** * Search Module */ -var Search = { - - _index : null, - _queued_query : null, - _pulse_status : -1, - - htmlToText : function(htmlString) { - var virtualDocument = document.implementation.createHTMLDocument('virtual'); - var htmlElement = $(htmlString, virtualDocument); - htmlElement.find('.headerlink').remove(); - docContent = htmlElement.find('[role=main]')[0]; - if(docContent === undefined) { - console.warn("Content block not found. Sphinx search tries to obtain it " + - "via '[role=main]'. Could you check your theme or template."); - return ""; - } - return docContent.textContent || docContent.innerText; +const Search = { + _index: null, + _queued_query: null, + _pulse_status: -1, + + htmlToText: (htmlString) => { + const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html'); + htmlElement.querySelectorAll(".headerlink").forEach((el) => { el.remove() }); + const docContent = htmlElement.querySelector('[role="main"]'); + if (docContent !== undefined) return docContent.textContent; + console.warn( + "Content block not found. Sphinx search tries to obtain it via '[role=main]'. Could you check your theme or template." + ); + return ""; }, - init : function() { - var params = $.getQueryParameters(); - if (params.q) { - var query = params.q[0]; - $('input[name="q"]')[0].value = query; - this.performSearch(query); - } + init: () => { + const query = new URLSearchParams(window.location.search).get("q"); + document + .querySelectorAll('input[name="q"]') + .forEach((el) => (el.value = query)); + if (query) Search.performSearch(query); }, - loadIndex : function(url) { - $.ajax({type: "GET", url: url, data: null, - dataType: "script", cache: true, - complete: function(jqxhr, textstatus) { - if (textstatus != "success") { - document.getElementById("searchindexloader").src = url; - } - }}); - }, + loadIndex: (url) => + (document.body.appendChild(document.createElement("script")).src = url), - setIndex : function(index) { - var q; - this._index = index; - if ((q = this._queued_query) !== null) { - this._queued_query = null; - Search.query(q); + setIndex: (index) => { + Search._index = index; + if (Search._queued_query !== null) { + const query = Search._queued_query; + Search._queued_query = null; + Search.query(query); } }, - hasIndex : function() { - return this._index !== null; - }, + hasIndex: () => Search._index !== null, - deferQuery : function(query) { - this._queued_query = query; - }, + deferQuery: (query) => (Search._queued_query = query), - stopPulse : function() { - this._pulse_status = 0; - }, + stopPulse: () => (Search._pulse_status = -1), - startPulse : function() { - if (this._pulse_status >= 0) - return; - function pulse() { - var i; + startPulse: () => { + if (Search._pulse_status >= 0) return; + + const pulse = () => { Search._pulse_status = (Search._pulse_status + 1) % 4; - var dotString = ''; - for (i = 0; i < Search._pulse_status; i++) - dotString += '.'; - Search.dots.text(dotString); - if (Search._pulse_status > -1) - window.setTimeout(pulse, 500); - } + Search.dots.innerText = ".".repeat(Search._pulse_status); + if (Search._pulse_status >= 0) window.setTimeout(pulse, 500); + }; pulse(); }, /** * perform a search for something (or wait until index is loaded) */ - performSearch : function(query) { + performSearch: (query) => { // create the required interface elements - this.out = $('#search-results'); - this.title = $('

' + _('Searching') + '

').appendTo(this.out); - this.dots = $('').appendTo(this.title); - this.status = $('

 

').appendTo(this.out); - this.output = $(' +
    +
  • + modules.helper + +
  • +
  • + modules.models + +
  • +
  • + modules.netgear + +
  • +
  • + modules.settings + +
+

N

+ + + +
+ +

P

+ + +
+

R

+

S

+
@@ -169,7 +287,7 @@

Quick search

- +
@@ -189,7 +307,7 @@

Navigation

\ No newline at end of file diff --git a/docs/index.html b/docs/index.html index 7b19791..b43bb34 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,10 +1,10 @@ - + - + Welcome to NetScan’s documentation! — NetScan documentation @@ -13,6 +13,7 @@ + @@ -42,157 +43,288 @@

Navigation

-

Welcome to NetScan’s documentation!

+

Welcome to NetScan’s documentation!

-

NetScan

+

NetScan

+
+
+analyzer.network_monitor(module: SupportedModules, init: bool = True) NoReturn
+

Monitor devices connected to the network.

+
+
Parameters:
+
    +
  • module – Module to scan. Currently, supports any network on a Netgear router or At&t networks.

  • +
  • init – Takes a boolean value to create a snapshot file or actually monitor the network.

  • +
+
+
+
+ +
+
+

At&t

-
-class analyzer.LocalIPScan(router_pass: Optional[str] = None)
+
+class modules.att.Device(dictionary: dict)
+

Convert dictionary into a device object.

+
>>> Device
+
+
+
+ +
+
+modules.att.create_snapshot() NoReturn
+

Creates a snapshot.json which is used to determine the known and unknown devices.

+
+ +
+
+modules.att.format_key(key: str) str
+

Format the key to match the Device object.

+
+ +
+
+modules.att.generate_dataframe() DataFrame
+

Generate a dataframe using the devices information from router web page.

+
+
Returns:
+

Devices list as a data frame.

+
+
Return type:
+

DataFrame

+
+
+
+ +
+
+modules.att.get_attached_devices() Generator[Device]
+

Get all devices connected to the router.

+
+
Yields:
+

Generator[Device] – Yields each device information as a Device object.

+
+
+
+ +
+
+modules.att.get_ipaddress() str
+

Get network id from the current IP address.

+
+ +
+
+modules.att.run() NoReturn
+

Trigger to initiate a Network Scan and block the devices that are not present in snapshot.json file.

+
+ +
+
+

Netgear

+
+
+class modules.netgear.LocalIPScan

Connector to scan devices in the same IP range using Netgear API.

>>> LocalIPScan
 
-
-allow(device: Union[str, pynetgear.Device]) Optional[pynetgear.Device]
+
+allow(device: Union[str, Device]) Optional[Device]

Allows internet access to a device.

-
Parameters
+
Parameters:

device – Takes device name or Device object as an argument.

-
Returns
+
Returns:

Returns the device object received from get_device_by_name() method.

-
Return type
-

Device

+
Return type:
+

Device

-
-always_allow(device: pynetgear.Device) NoReturn
+
+always_allow(device: Device) NoReturn

Allows internet access to a device.

-

Saves the device name to snapshot.json to not block in future. +

Saves the device name to snapshot.json to not block in the future. Removes the device name from blocked.json if an entry is present.

-
Parameters
+
Parameters:

device – Takes device name or Device object as an argument

-
-block(device: Union[str, pynetgear.Device]) Optional[pynetgear.Device]
+
+block(device: Union[str, Device]) Optional[Device]

Blocks internet access to a device.

-
Parameters
+
Parameters:

device – Takes device name or Device object as an argument.

-
Returns
+
Returns:

Returns the device object received from get_device_by_name() method.

-
Return type
-

Device

+
Return type:
+

Device

-
-create_snapshot() NoReturn
+
+create_snapshot() NoReturn

Creates a snapshot.json which is used to determine the known and unknown devices.

-
-run(block: bool = False) NoReturn
+
+run(block: bool = False) NoReturn

Trigger to initiate a Network Scan and block the devices that are not present in snapshot.json file.

+
+
+

Helper

-
-analyzer.custom_time(*args: logging.Formatter) time.struct_time
+
+modules.helper.custom_time(*args: Formatter) struct_time

Creates custom timezone for logging which gets used only when invoked by Docker.

This is used only when triggered within a docker container as it uses UTC timezone.

-
Parameters
+
Parameters:

*args – Takes Formatter object and current epoch time as arguments passed by formatTime from logging.

-
Returns
+
Returns:

A struct_time object which is a tuple of: current year, month, day, hour, minute, second, weekday, year day and dst (Daylight Saving Time)

-
Return type
+
Return type:

struct_time

-
-analyzer.device_name() str
-

Gets the device name for MacOS and Windows.

-
- -
-
-analyzer.extract_str(input_: AnyStr) str
-

Extracts strings from the received input.

+
+modules.helper.notify(msg: str) NoReturn
+

Send an email notification when there is a threat.

-
Parameters
-

input_ – Takes a string as argument.

-
-
Returns
-

A string after removing special characters.

-
-
Return type
-

str

+
Parameters:
+

msg – Message that has to be sent.

-
-
-analyzer.get_ssid() Optional[str]
-

Checks the current operating system and runs the appropriate command to get the SSID of the access point.

-
-
Returns
-

SSID of the access point/router which is being accessed.

-
-
Return type
-

str

-
-
+
+
+

Models

+
+
+class modules.models.DeviceStatus(value)
+

Device status strings for allow or block.

+
+
+allow: str = 'Allow'
+
+ +
+
+block: str = 'Block'
+
+
-
-
-analyzer.send_sms(msg: str) NoReturn
-

Sens an SMS notification when invoked by the run method.

-
-
Parameters
-

msg – Message that has to be sent.

-
-
+
+
+class modules.models.SupportedModules(value)
+

Supported modules are At&t and Netgear.

+
+
+att: str = 'At&t'
+
+ +
+
+netgear: str = 'Netgear'
+
+ +
+ +
+
+

Settings

+
+
+class modules.settings.Config
+

Wrapper for all the environment variables.

+
+
+blocked = 'fileio/blocked.yaml'
+
+ +
+
+docker = None
+
+ +
+
+gmail_pass = None
+
+ +
+
+gmail_user = None
+
+ +
+
+phone = None
+
+ +
+
+recipient = None
+
+ +
+
+router_pass = None
+
+ +
+
+snapshot = 'fileio/snapshot.json'
+
+
-

Indices and tables

+

Indices and tables

@@ -256,7 +397,7 @@

Navigation

\ No newline at end of file diff --git a/docs/objects.inv b/docs/objects.inv index c7de258..24e60d1 100644 --- a/docs/objects.inv +++ b/docs/objects.inv @@ -2,4 +2,4 @@ # Project: NetScan # Version: # The remainder of this file is compressed using zlib. -xڝKN09lSmwHtQ.XZS{Hy&a5'!JPRv?7cLATz]tYz#+0lu2@$c7|}? zjw$/C&9&Vri_jT(L6K0ȱqB%Y:?-KA+ELzB<"wz ltFv-onDvez3+5v{:WJHK|^_ָ-#JE R~Pd%-"Vu R͹AW6FJ!V r}I܆rۛ"YE)&7PEdMڇyf o|]REt%Brn1l lׇ1#ɛs0O;qLc \ No newline at end of file diff --git a/docs/py-modindex.html b/docs/py-modindex.html index f144b59..c1ede0c 100644 --- a/docs/py-modindex.html +++ b/docs/py-modindex.html @@ -1,7 +1,7 @@ - + @@ -12,16 +12,13 @@ + - - - +
@@ -97,7 +129,7 @@

Navigation

\ No newline at end of file diff --git a/docs/search.html b/docs/search.html index ce516e7..733416b 100644 --- a/docs/search.html +++ b/docs/search.html @@ -1,7 +1,7 @@ - + @@ -13,6 +13,7 @@ + @@ -44,13 +45,14 @@

Navigation

Search

-
- +

@@ -97,7 +99,7 @@

Navigation

\ No newline at end of file diff --git a/docs/searchindex.js b/docs/searchindex.js index d67035b..5bdcde1 100644 --- a/docs/searchindex.js +++ b/docs/searchindex.js @@ -1 +1 @@ -Search.setIndex({docnames:["README","index"],envversion:{"sphinx.domains.c":2,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":4,"sphinx.domains.index":1,"sphinx.domains.javascript":2,"sphinx.domains.math":2,"sphinx.domains.python":3,"sphinx.domains.rst":2,"sphinx.domains.std":2,sphinx:56},filenames:["README.md","index.rst"],objects:{"":{analyzer:[1,0,0,"-"]},"analyzer.LocalIPScan":{allow:[1,2,1,""],always_allow:[1,2,1,""],block:[1,2,1,""],create_snapshot:[1,2,1,""],run:[1,2,1,""]},analyzer:{LocalIPScan:[1,1,1,""],custom_time:[1,3,1,""],device_name:[1,3,1,""],extract_str:[1,3,1,""],get_ssid:[1,3,1,""],send_sms:[1,3,1,""]}},objnames:{"0":["py","module","Python module"],"1":["py","class","Python class"],"2":["py","method","Python method"],"3":["py","function","Python function"]},objtypes:{"0":"py:module","1":"py:class","2":"py:method","3":"py:function"},terms:{"class":1,"return":1,A:1,access:1,accordingli:0,ad:0,addit:0,address:0,after:1,alert:0,allow:1,always_allow:1,an:1,analyz:[0,1],anystr:1,api:1,app:0,appropri:1,ar:1,arg:1,argument:1,being:1,block:[0,1],bool:1,build:0,can:0,charact:1,check:1,command:1,connect:0,connector:1,contain:1,creat:1,create_snapshot:1,current:1,custom:1,custom_tim:1,dai:1,daylight:1,determin:1,devic:[0,1],device_nam:1,displai:0,docker:1,dst:1,entri:1,epoch:1,etc:0,even:0,extract:1,extract_str:1,fals:1,featur:0,fi:0,file:1,formatt:1,formattim:1,from:[0,1],futur:1,get:1,get_device_by_nam:1,get_ssid:1,ha:1,help:0,hour:1,index:1,info:0,initi:1,input:1,input_:1,internet:1,intrud:0,invok:1,ip:[0,1],json:1,known:1,let:0,localipscan:1,log:1,mac:0,maco:1,messag:1,method:1,minut:1,modul:1,month:1,msg:1,name:1,netgear:1,network:[0,1],none:1,noreturn:1,notif:1,object:1,onli:1,oper:1,option:[0,1],page:1,paramet:1,pass:1,ping:0,plain:0,point:1,present:1,progress:0,pynetgear:1,rang:1,receiv:1,remov:[0,1],router:[0,1],router_pass:1,run:[0,1],same:1,save:1,scan:1,scanner:0,search:1,second:1,sen:1,send_sm:1,sent:1,setup:1,signal:0,sm:1,snapshot:1,special:1,specif:0,specifi:0,speed:0,ssid:1,str:1,string:1,struct_tim:1,system:1,t:0,take:1,thi:[0,1],time:1,timezon:1,trigger:1,tupl:1,type:1,union:1,unknown:1,us:1,user:0,utc:1,weekdai:1,when:1,which:1,wi:0,window:1,within:1,year:1},titles:["NetScan","Welcome to NetScan\u2019s documentation!"],titleterms:{command:0,docker:0,document:1,indic:1,me:1,netscan:[0,1],read:1,refer:0,repositori:0,runbook:0,s:1,setup:0,tabl:1,welcom:1}}) \ No newline at end of file +Search.setIndex({"docnames": ["README", "index"], "filenames": ["README.md", "index.rst"], "titles": ["NetScan", "Welcome to NetScan\u2019s documentation!"], "terms": {"network": [0, 1], "scanner": 0, "analyz": [0, 1], "devic": [0, 1], "connect": [0, 1], "router": [0, 1], "alert": 0, "accordingli": 0, "thi": [0, 1], "app": 0, "can": 0, "displai": 0, "intrud": 0, "ip": [0, 1], "address": [0, 1], "mac": 0, "let": 0, "user": 0, "ping": 0, "even": 0, "block": [0, 1], "featur": 0, "help": 0, "remov": [0, 1], "specifi": 0, "specif": 0, "i": [0, 1], "current": [0, 1], "avail": 0, "onli": [0, 1], "netgear": 0, "docstr": 0, "format": [0, 1], "googl": 0, "style": 0, "convent": 0, "pep": 0, "8": 0, "clean": 0, "pre": 0, "commit": 0, "hook": 0, "flake8": 0, "isort": 0, "requir": 0, "python": 0, "m": 0, "pip": 0, "instal": 0, "changelog": 0, "gener": [0, 1], "usag": 0, "revers": 0, "f": 0, "release_not": 0, "rst": 0, "t": 0, "precommit": 0, "ensur": 0, "doc": 0, "creation": 0, "ar": [0, 1], "run": [0, 1], "everi": 0, "sphinx": 0, "5": 0, "1": 0, "recommonmark": 0, "all": [0, 1], "file": [0, 1], "http": 0, "thevickypedia": 0, "github": 0, "io": 0, "code": 1, "standard": 1, "releas": 1, "note": 1, "lint": 1, "runbook": 1, "network_monitor": 1, "modul": 1, "supportedmodul": 1, "init": 1, "bool": 1, "true": 1, "noreturn": 1, "monitor": 1, "paramet": 1, "scan": 1, "support": 1, "ani": 1, "take": 1, "boolean": 1, "valu": 1, "creat": 1, "snapshot": 1, "actual": 1, "class": 1, "att": 1, "dictionari": 1, "dict": 1, "convert": 1, "object": 1, "create_snapshot": 1, "json": 1, "which": 1, "us": 1, "determin": 1, "known": 1, "unknown": 1, "format_kei": 1, "kei": 1, "str": 1, "match": 1, "generate_datafram": 1, "datafram": 1, "inform": 1, "from": 1, "web": 1, "page": 1, "return": 1, "list": 1, "data": 1, "frame": 1, "type": 1, "get_attached_devic": 1, "get": 1, "yield": 1, "each": 1, "get_ipaddress": 1, "id": 1, "trigger": 1, "initi": 1, "present": 1, "localipscan": 1, "connector": 1, "same": 1, "rang": 1, "api": 1, "allow": 1, "union": 1, "option": 1, "internet": 1, "access": 1, "name": 1, "an": 1, "argument": 1, "receiv": 1, "get_device_by_nam": 1, "method": 1, "always_allow": 1, "save": 1, "futur": 1, "entri": 1, "fals": 1, "custom_tim": 1, "arg": 1, "formatt": 1, "struct_tim": 1, "custom": 1, "timezon": 1, "log": 1, "when": 1, "invok": 1, "docker": 1, "within": 1, "contain": 1, "utc": 1, "epoch": 1, "time": 1, "pass": 1, "formattim": 1, "A": 1, "tupl": 1, "year": 1, "month": 1, "dai": 1, "hour": 1, "minut": 1, "second": 1, "weekdai": 1, "dst": 1, "daylight": 1, "notifi": 1, "msg": 1, "send": 1, "email": 1, "notif": 1, "threat": 1, "messag": 1, "ha": 1, "sent": 1, "devicestatu": 1, "statu": 1, "string": 1, "config": 1, "wrapper": 1, "environ": 1, "variabl": 1, "fileio": 1, "yaml": 1, "none": 1, "gmail_pass": 1, "gmail_us": 1, "phone": 1, "recipi": 1, "router_pass": 1, "index": 1, "search": 1}, "objects": {"": [[1, 0, 0, "-", "analyzer"]], "analyzer": [[1, 1, 1, "", "network_monitor"]], "modules": [[1, 0, 0, "-", "att"], [1, 0, 0, "-", "helper"], [1, 0, 0, "-", "models"], [1, 0, 0, "-", "netgear"], [1, 0, 0, "-", "settings"]], "modules.att": [[1, 2, 1, "", "Device"], [1, 1, 1, "", "create_snapshot"], [1, 1, 1, "", "format_key"], [1, 1, 1, "", "generate_dataframe"], [1, 1, 1, "", "get_attached_devices"], [1, 1, 1, "", "get_ipaddress"], [1, 1, 1, "", "run"]], "modules.helper": [[1, 1, 1, "", "custom_time"], [1, 1, 1, "", "notify"]], "modules.models": [[1, 2, 1, "", "DeviceStatus"], [1, 2, 1, "", "SupportedModules"]], "modules.models.DeviceStatus": [[1, 3, 1, "", "allow"], [1, 3, 1, "", "block"]], "modules.models.SupportedModules": [[1, 3, 1, "", "att"], [1, 3, 1, "", "netgear"]], "modules.netgear": [[1, 2, 1, "", "LocalIPScan"]], "modules.netgear.LocalIPScan": [[1, 4, 1, "", "allow"], [1, 4, 1, "", "always_allow"], [1, 4, 1, "", "block"], [1, 4, 1, "", "create_snapshot"], [1, 4, 1, "", "run"]], "modules.settings": [[1, 2, 1, "", "Config"]], "modules.settings.Config": [[1, 3, 1, "", "blocked"], [1, 3, 1, "", "docker"], [1, 3, 1, "", "gmail_pass"], [1, 3, 1, "", "gmail_user"], [1, 3, 1, "", "phone"], [1, 3, 1, "", "recipient"], [1, 3, 1, "", "router_pass"], [1, 3, 1, "", "snapshot"]]}, "objtypes": {"0": "py:module", "1": "py:function", "2": "py:class", "3": "py:attribute", "4": "py:method"}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "function", "Python function"], "2": ["py", "class", "Python class"], "3": ["py", "attribute", "Python attribute"], "4": ["py", "method", "Python method"]}, "titleterms": {"netscan": [0, 1], "code": 0, "standard": 0, "releas": 0, "note": 0, "lint": 0, "runbook": 0, "welcom": 1, "": 1, "document": 1, "read": 1, "me": 1, "At": 1, "t": 1, "netgear": 1, "helper": 1, "model": 1, "set": 1, "indic": 1, "tabl": 1}, "envversion": {"sphinx.domains.c": 2, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 6, "sphinx.domains.index": 1, "sphinx.domains.javascript": 2, "sphinx.domains.math": 2, "sphinx.domains.python": 3, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx": 56}}) \ No newline at end of file diff --git a/doc_generator/Makefile b/docs_gen/Makefile similarity index 100% rename from doc_generator/Makefile rename to docs_gen/Makefile diff --git a/doc_generator/conf.py b/docs_gen/conf.py similarity index 100% rename from doc_generator/conf.py rename to docs_gen/conf.py diff --git a/doc_generator/index.rst b/docs_gen/index.rst similarity index 56% rename from doc_generator/index.rst rename to docs_gen/index.rst index 85bed8e..d788cb7 100644 --- a/doc_generator/index.rst +++ b/docs_gen/index.rst @@ -19,6 +19,41 @@ NetScan :members: :undoc-members: +At&t +==== + +.. automodule:: modules.att + :members: + :undoc-members: + +Netgear +======= + +.. automodule:: modules.netgear + :members: + :undoc-members: + +Helper +====== + +.. automodule:: modules.helper + :members: + :undoc-members: + +Models +====== + +.. automodule:: modules.models + :members: + :undoc-members: + +Settings +======== + +.. automodule:: modules.settings + :members: + :undoc-members: + Indices and tables ================== diff --git a/doc_generator/make.bat b/docs_gen/make.bat similarity index 95% rename from doc_generator/make.bat rename to docs_gen/make.bat index 2119f51..922152e 100644 --- a/doc_generator/make.bat +++ b/docs_gen/make.bat @@ -1,35 +1,35 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/gen_docs.sh b/gen_docs.sh index 57b6de7..fe281ef 100644 --- a/gen_docs.sh +++ b/gen_docs.sh @@ -4,6 +4,6 @@ set -e rm -rf docs mkdir docs -mkdir -p doc_generator/_static # creates a _static folder if unavailable -cp README.md doc_generator && cd doc_generator && make clean html && mv _build/html/* ../docs && rm README.md +mkdir -p docs_gen/_static # creates a _static folder if unavailable +cp README.md docs_gen && cd docs_gen && make clean html && mv _build/html/* ../docs && rm README.md touch ../docs/.nojekyll \ No newline at end of file diff --git a/modules/att.py b/modules/att.py new file mode 100644 index 0000000..a0056fb --- /dev/null +++ b/modules/att.py @@ -0,0 +1,144 @@ +import json +import os +import socket +from collections.abc import Generator +from typing import Any, NoReturn, Optional, Union + +import pandas +import requests +from pandas import DataFrame + +from modules.helper import notify +from modules.settings import LOGGER, config + +SOURCE = "http://{NETWORK_ID}.254/cgi-bin/devices.ha" + + +def get_ipaddress() -> str: + """Get network id from the current IP address.""" + socket_ = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + socket_.connect(("8.8.8.8", 80)) + ip_address = socket_.getsockname()[0] + network_id = '.'.join(ip_address.split('.')[0:3]) + socket_.close() + except OSError as error: + LOGGER.warning(error) + network_id = "192.168.1" + return network_id + + +SOURCE = SOURCE.format(NETWORK_ID=get_ipaddress()) + + +class Device: + """Convert dictionary into a device object. + + >>> Device + + """ + + def __init__(self, dictionary: dict): + """Set dictionary keys as attributes of Device object. + + Args: + dictionary: Takes the input dictionary as an argument. + """ + self.mac_address: Optional[str] = None + self.ipv4_address: Optional[str] = None + self.name: Optional[str] = None + self.last_activity: Optional[str] = None + self.status: Optional[str] = None + self.allocation: Optional[str] = None + self.connection_type: Optional[str] = None + self.connection_speed: Optional[Union[float, Any]] = None + self.mesh_client: Optional[str] = None + for key in dictionary: + setattr(self, key, dictionary[key]) + + +def generate_dataframe() -> DataFrame: + """Generate a dataframe using the devices information from router web page. + + Returns: + DataFrame: + Devices list as a data frame. + """ + # pandas.set_option('display.max_rows', None) + try: + response = requests.get(url=SOURCE) + except requests.RequestException as error: + LOGGER.error(error) + raise ConnectionError(error.args) + else: + if response.ok: + html_source = response.text + html_tables = pandas.read_html(html_source) + return html_tables[0] + else: + LOGGER.error("[%s] - %s" % (response.status_code, response.text)) + + +def format_key(key: str) -> str: + """Format the key to match the Device object.""" + return key.lower().replace(' ', '_').replace('-', '_') + + +def get_attached_devices() -> Generator[Device]: + """Get all devices connected to the router. + + Yields: + Generator[Device]: + Yields each device information as a Device object. + """ + device_info = {} + dataframe = generate_dataframe() + if dataframe is None: + return + for value in dataframe.values: + if str(value[0]) == "nan": + yield Device(device_info) + device_info = {} + elif value[0] == "IPv4 Address / Name": + key = value[0].split('/') + val = value[1].split('/') + device_info[format_key(key[0].strip())] = val[0].strip() + device_info[format_key(key[1].strip())] = val[1].strip() + else: + device_info[format_key(value[0])] = value[1] + + +def create_snapshot() -> NoReturn: + """Creates a snapshot.json which is used to determine the known and unknown devices.""" + devices = {} + for device in get_attached_devices(): + if device.ipv4_address: + devices[device.ipv4_address] = [str(device.name), str(device.connection_type), str(device.last_activity)] + LOGGER.info('Number of devices connected: %d' % len(devices.keys())) + with open(config.snapshot, 'w') as file: + json.dump(devices, file, indent=2) + + +def run() -> NoReturn: + """Trigger to initiate a Network Scan and block the devices that are not present in ``snapshot.json`` file.""" + if not os.path.isfile(config.snapshot): + LOGGER.error("'%s' not found. Please run `create_snapshot()` and review it." % config.snapshot) + raise FileNotFoundError( + "'%s' is required" % config.snapshot + ) + with open(config.snapshot) as file: + device_list = json.load(file) + stored_ips = list(device_list.keys()) + threat = '' + for device in get_attached_devices(): + if device.ipv4_address and device.ipv4_address not in stored_ips: + # SOURCE = "http://{NETWORK_ID}.254/cgi-bin/devices.ha" + LOGGER.warning('{name} [{ip}: {mac}] is connected to your network.'.format(name=device.name, + mac=device.mac_address, + ip=device.ipv4_address)) + threat += '\nName: {name}\nIP: {ip}\nMAC: {mac}'.format(name=device.name, mac=device.mac_address, + ip=device.ipv4_address) + if threat: + notify(msg=threat) + else: + LOGGER.info('NetScan has completed. No threats found on your network.') diff --git a/modules/helper.py b/modules/helper.py new file mode 100644 index 0000000..52331ee --- /dev/null +++ b/modules/helper.py @@ -0,0 +1,49 @@ +import logging +import time +from datetime import datetime, timezone +from typing import NoReturn + +import gmailconnector + +from modules.settings import LOGGER, config + + +def custom_time(*args: logging.Formatter or time.time) -> time.struct_time: + """Creates custom timezone for ``logging`` which gets used only when invoked by ``Docker``. + + This is used only when triggered within a ``docker container`` as it uses UTC timezone. + + Args: + *args: Takes ``Formatter`` object and current epoch time as arguments passed by ``formatTime`` from ``logging``. + + Returns: + struct_time: + A struct_time object which is a tuple of: + **current year, month, day, hour, minute, second, weekday, year day and dst** *(Daylight Saving Time)* + """ + LOGGER.debug(args) + local_timezone = datetime.now(tz=timezone.utc).astimezone().tzinfo + return datetime.now().astimezone(tz=local_timezone).timetuple() + + +if config.docker: + logging.Formatter.converter = custom_time + + +def notify(msg: str) -> NoReturn: + """Send an email notification when there is a threat. + + Args: + msg: Message that has to be sent. + """ + if config.gmail_user and config.gmail_pass and config.recipient: + emailer = gmailconnector.SendEmail(gmail_user=config.gmail_user, + gmail_pass=config.gmail_pass) + response = emailer.send_email(recipient=config.recipient, + subject=f"Netscan Alert - {datetime.now().strftime('%C')}", body=msg) + if response.ok: + LOGGER.info("Firewall alert has been sent to '%s'" % config.phone) + else: + LOGGER.error("Failed to send a notification.\n%s" % response.body) + else: + LOGGER.info("Env variables not found to trigger notification.") diff --git a/modules/models.py b/modules/models.py new file mode 100644 index 0000000..89fb964 --- /dev/null +++ b/modules/models.py @@ -0,0 +1,15 @@ +from enum import Enum + + +class DeviceStatus(str, Enum): + """Device status strings for allow or block.""" + + allow: str = "Allow" + block: str = "Block" + + +class SupportedModules(str, Enum): + """Supported modules are At&t and Netgear.""" + + att: str = "At&t" + netgear: str = "Netgear" diff --git a/modules/netgear.py b/modules/netgear.py new file mode 100644 index 0000000..7cabbd2 --- /dev/null +++ b/modules/netgear.py @@ -0,0 +1,211 @@ +import copy +import json +import os +import time +from typing import NoReturn, Union + +import yaml +from pynetgear import Device, Netgear + +from modules.helper import notify +from modules.models import DeviceStatus +from modules.settings import LOGGER, config + + +class LocalIPScan: + """Connector to scan devices in the same IP range using ``Netgear API``. + + >>> LocalIPScan + + """ + + def __init__(self): + """Gets local host devices connected to the same network range.""" + self.netgear = Netgear(password=config.router_pass) + + def _get_devices(self) -> Device: + """Scans the Netgear router for connected devices and the devices' information. + + Returns: + Device: + Returns list of devices connected to the router and the connection information. + """ + LOGGER.info('Getting devices connected to your network.') + if devices := self.netgear.get_attached_devices(): + return devices + else: + text = "'router_pass' is invalid" if config.router_pass else "'router_pass' is required for netgear network" + raise ValueError("\n\n" + text) + + def create_snapshot(self) -> NoReturn: + """Creates a snapshot.json which is used to determine the known and unknown devices.""" + LOGGER.warning("Creating a snapshot will capture the current list of devices connected to your network at" + " this moment.") + LOGGER.warning("This capture will be used to alert/block when new devices are connected. So, please review " + "the '%s' manually and remove the devices that aren't recognized." % config.snapshot) + devices = {} + for device in self._get_devices(): + if device.ip: # Only look for currently connected devices + devices[device.ip] = [device.name, device.type, device.allow_or_block] + LOGGER.info('Number of devices connected: %d' % len(devices.keys())) + with open(config.snapshot, 'w') as file: + json.dump(devices, file, indent=2) + + def _get_device_by_name(self, name: str) -> Device: + """Calls the ``get_devices()`` method and checks if the given device is available in the list. + + Args: + name: Takes device name as argument. + + Returns: + Device: + Returns device information as a Device object. + """ + for device in self._get_devices(): + if device.name == name: + return device + + def _get_device_obj(self, device: str): + """Identify device object using the device name as a string.""" + if isinstance(device, str): + tmp = device + LOGGER.info('Looking information on %s' % device) + if device := self._get_device_by_name(name=device): + return device + else: + LOGGER.error('Device: %s is not connected to your network.' % tmp) + else: + return device + + def allow(self, device: Union[str, Device]) -> Union[Device, None]: + """Allows internet access to a device. + + Args: + device: Takes device name or Device object as an argument. + + Returns: + Device: + Returns the device object received from ``get_device_by_name()`` method. + """ + if device := self._get_device_obj(device=device): + LOGGER.info("Granting internet access to '%s'" % device.name) + self.netgear.allow_block_device(mac_addr=device.mac, device_status=DeviceStatus.allow) + return device + + def block(self, device: Union[str, Device]) -> Union[Device, None]: + """Blocks internet access to a device. + + Args: + device: Takes device name or Device object as an argument. + + Returns: + Device: + Returns the device object received from ``get_device_by_name()`` method. + """ + device = self._get_device_obj(device=device) + LOGGER.info("Blocking internet access to '%s'" % device.name) + self.netgear.allow_block_device(mac_addr=device.mac, device_status=DeviceStatus.block) + return device + + @staticmethod + def _dump_blocked(device: Device) -> NoReturn: + """Converts device object to a dictionary and dumps it into ``blocked.json`` file. + + Args: + device: Takes Device object as an argument. + """ + LOGGER.info("Details of '%s' has been stored in %s" % (device.name, config.blocked)) + with open(config.blocked, 'a') as file: + # noinspection PyProtectedMember + dictionary = {time.time(): device._asdict()} + yaml.dump(dictionary, file, allow_unicode=True, default_flow_style=False, sort_keys=False) + + @staticmethod + def _get_blocked(): + if os.path.isfile(config.blocked): + with open(config.blocked) as file: + try: + blocked_devices = yaml.load(stream=file, Loader=yaml.FullLoader) or {} + except yaml.YAMLError as error: + LOGGER.error(error) + else: + for epoch, device_info in blocked_devices.items(): + yield device_info.get('mac') + + def always_allow(self, device: Device or str) -> NoReturn: + """Allows internet access to a device. + + Saves the device name to ``snapshot.json`` to not block in the future. + Removes the device name from ``blocked.json`` if an entry is present. + + Args: + device: Takes device name or Device object as an argument + """ + if isinstance(device, Device): + device = device.name # converts Device object to string + if not (device := self.allow(device=device)): # converts string to Device object + return + + with open(config.snapshot, 'r+') as file: + data = json.load(file) + file.seek(0) + if device.ip and device.ip in list(data.keys()): + LOGGER.info("'%s' is a part of allow list." % device.name) + data[device.ip][-1] = DeviceStatus.allow + LOGGER.info("Setting status to Allow for '%s' in %s" % (device.name, config.snapshot)) + elif device.ip: + data[device.ip] = [device.name, device.type, device.allow_or_block] + LOGGER.info("Adding '%s' to %s" % (device.name, config.snapshot)) + json.dump(data, file, indent=2) + file.truncate() + + if os.path.isfile(config.blocked): + with open(config.blocked) as file: + try: + blocked_devices = yaml.load(stream=file, Loader=yaml.FullLoader) or {} + except yaml.YAMLError as error: + LOGGER.error(error) + return + blocked_copy = copy.deepcopy(blocked_devices) + for epoch, device_info in blocked_copy.items(): # convert to a list of dict + if device_info.get('mac') == device.mac: + LOGGER.info("Removing '%s' from %s" % (device.name, config.blocked)) + del blocked_devices[epoch] + file.seek(0) + file.truncate() + if blocked_devices: + yaml.dump(blocked_devices, file, indent=2) + + def run(self, block: bool = False) -> NoReturn: + """Trigger to initiate a Network Scan and block the devices that are not present in ``snapshot.json`` file.""" + if not os.path.isfile(config.snapshot): + LOGGER.error("'%s' not found. Please generate one and review it." % config.snapshot) + raise FileNotFoundError( + '%s is required' % config.snapshot + ) + with open(config.snapshot) as file: + device_list = json.load(file) + stored_ips = list(device_list.keys()) + threat = '' + blocked = list(self._get_blocked()) + for device in self._get_devices(): + if device.ip and device.ip not in stored_ips: + LOGGER.warning("{name} with MAC address {mac} and a signal strength of {signal}% has connected to your " + "network.".format(name=device.name, mac=device.mac, signal=device.signal)) + + if device.allow_or_block == DeviceStatus.allow: + if block: + self.block(device=device) + if device.mac not in blocked: + self._dump_blocked(device=device) + else: + LOGGER.info("'%s' is a part of deny list." % device.name) + threat += '\nName: {name}\nIP: {ip}\nMAC: {mac}'.format(name=device.name, ip=device.ip, + mac=device.mac) + else: + LOGGER.info("'%s' does not have internet access." % device.name) + + if threat: + notify(msg=threat) + else: + LOGGER.info('NetScan has completed. No threats found on your network.') diff --git a/modules/settings.py b/modules/settings.py new file mode 100644 index 0000000..24e1990 --- /dev/null +++ b/modules/settings.py @@ -0,0 +1,34 @@ +import logging +import os + +import dotenv + +dotenv.load_dotenv(dotenv_path=".env") + +if not os.path.isdir('fileio'): + os.makedirs('fileio') + +LOGGER = logging.getLogger(__name__) +handler = logging.StreamHandler() +handler.setFormatter(fmt=logging.Formatter( + fmt="%(asctime)s - [%(levelname)s] - %(name)s - %(funcName)s - Line: %(lineno)d - %(message)s", + datefmt='%b-%d-%Y %H:%M:%S' +)) +LOGGER.setLevel(level=logging.DEBUG) +LOGGER.addHandler(hdlr=handler) + + +class Config: + """Wrapper for all the environment variables.""" + + router_pass = os.environ.get('ROUTER_PASS') or os.environ.get('router_pass') + gmail_user = os.environ.get('GMAIL_USER') or os.environ.get('gmail_user') + gmail_pass = os.environ.get('GMAIL_PASS') or os.environ.get('gmail_pass') + recipient = os.environ.get('RECIPIENT') or os.environ.get('recipient') + docker = os.environ.get('DOCKER') or os.environ.get('docker') + phone = os.environ.get('PHONE') or os.environ.get('phone') + snapshot = os.path.join('fileio', 'snapshot.json') + blocked = os.path.join('fileio', 'blocked.yaml') + + +config = Config() diff --git a/release_notes.rst b/release_notes.rst new file mode 100644 index 0000000..93845e7 --- /dev/null +++ b/release_notes.rst @@ -0,0 +1,66 @@ +Release Notes +============= + +0.1.5 (04/16/2022) +------------------ +- Update docstrings + +0.1.4 (04/16/2022) +------------------ +- Make snapshot a JSON file instead of yaml +- Filter block devices more appropriately +- Change timezone conversion method + +0.1.3 (11/14/2021) +------------------ +- Upgrade `gmailconnector` version +- Auto create `_static` directory if unavailable + +0.1.2 (08/27/2021) +------------------ +- Update `timezone` when invoked by `docker build` + +0.1.1 (08/26/2021) +------------------ +- Add Dockerfile and update README.md + +0.1.0 (08/26/2021) +------------------ +- Add notifications when a threat is noticed on the connected SSID + +0.0.9 (08/26/2021) +------------------ +- Onboard sphinx autodocs +- Update README.md and docstrings + +0.0.8 (08/25/2021) +------------------ +- update README.md and remove first person references + +0.0.7 (08/25/2021) +------------------ +- add an option to always allow an unknown device + +0.0.6 (08/25/2021) +------------------ +- add `docstrings` + +0.0.5 (08/25/2021) +------------------ +- Add `ssid` and `logger` + +0.0.4 (08/25/2021) +------------------ +- Create a `LocalIPScan` to analyzer network and block devices + +0.0.3 (08/25/2021) +------------------ +- add requirements.txt + +0.0.2 (08/25/2021) +------------------ +- update README.md and add .gitignore + +0.0.1 (08/23/2021) +------------------ +- Initial commit diff --git a/requirements.txt b/requirements.txt index b8e3944..4fc25dc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ -gmail-connector -pynetgear==0.7.0 -python_dotenv==0.20.0 -PyYAML==5.4.1 -pytz==2021.1 -Sphinx==4.1.2 -recommonmark==0.7.1 -pre-commit==2.14.0 \ No newline at end of file +pynetgear==0.10.9 +python-dotenv +pytz +PyYAML +requests +pandas +lxml +gmail-connector \ No newline at end of file