diff options
Diffstat (limited to 'src/hydrilla/proxy/state.py')
-rw-r--r-- | src/hydrilla/proxy/state.py | 658 |
1 files changed, 658 insertions, 0 deletions
diff --git a/src/hydrilla/proxy/state.py b/src/hydrilla/proxy/state.py new file mode 100644 index 0000000..f73d01f --- /dev/null +++ b/src/hydrilla/proxy/state.py @@ -0,0 +1,658 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + +# Haketilo proxy data and configuration (interface definition through abstract +# class). +# +# This file is part of Hydrilla&Haketilo. +# +# Copyright (C) 2022 Wojtek Kosior +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# +# I, Wojtek Kosior, thereby promise not to sue for violation of this +# file's license. Although I request that you do not make use of this +# code in a proprietary program, I am not going to enforce this in +# court. + +""" +This module defines API for keeping track of all settings, rules, mappings and +resources. +""" + +import dataclasses as dc +import typing as t + +from pathlib import Path +from abc import ABC, abstractmethod +from enum import Enum +from datetime import datetime + +from immutables import Map + +from ..exceptions import HaketiloException +from ..versions import VerTuple +from ..url_patterns import ParsedPattern +from .. import item_infos +from .simple_dependency_satisfying import ImpossibleSituation + + +class EnabledStatus(Enum): + """ + ENABLED - User wished to always apply given mapping when it matches site's + URL. + + DISABLED - User wished to never apply given mapping. + + NO_MARK - User has not configured given mapping. + """ + ENABLED = 'E' + DISABLED = 'D' + NO_MARK = 'N' + + +class FrozenStatus(Enum): + """ + EXACT_VERSION - User wished to always use the same version of a mapping. + + REPOSITORY - User wished to always use a version of the mapping from the + same repository. + + NOT_FROZEN - User did not restrict updates of the mapping. + """ + EXACT_VERSION = 'E' + REPOSITORY = 'R' + NOT_FROZEN = 'N' + + @staticmethod + def make(letter: t.Optional[str]) -> t.Optional['FrozenStatus']: + if letter is None: + return None + + return FrozenStatus(letter) + + +class InstalledStatus(Enum): + """ + INSTALLED - Mapping's all files are present and mapping data is not going to + be automatically removed. + + NOT_INSTALLED - Some of the mapping's files might be absent. Mapping can be + automatically removed if it is orphaned. + + FAILED_TO_INSTALL - Same as "NOT_INSTALLED" but we additionally know that + the last automatic attempt to install mapping's files from repository + was unsuccessful. + """ + INSTALLED = 'I' + NOT_INSTALLED = 'N' + FAILED_TO_INSTALL = 'F' + + +class ActiveStatus(Enum): + """ + REQUIRED - Mapping version got active to fulfill a requirement of some (this + or another) explicitly enabled mapping. + + AUTO - Mapping version was activated automatically. + + NOT_ACTIVE - Mapping version is not currently being used. + """ + REQUIRED = 'R' + AUTO = 'A' + NOT_ACTIVE = 'N' + + +@dc.dataclass(frozen=True, unsafe_hash=True) +class Ref: + """....""" + id: str + + def __post_init__(self): + assert isinstance(self.id, str) + + +RefType = t.TypeVar('RefType', bound=Ref) + +class Store(ABC, t.Generic[RefType]): + @abstractmethod + def get(self, id) -> RefType: + ... + + +class RulePatternInvalid(HaketiloException): + pass + +@dc.dataclass(frozen=True) +class RuleDisplayInfo: + ref: 'RuleRef' + pattern: str + allow_scripts: bool + +# mypy needs to be corrected: +# https://stackoverflow.com/questions/70999513/conflict-between-mix-ins-for-abstract-dataclasses/70999704#70999704 +@dc.dataclass(frozen=True, unsafe_hash=True) # type: ignore[misc] +class RuleRef(Ref): + @abstractmethod + def remove(self) -> None: + ... + + @abstractmethod + def update( + self, + *, + pattern: t.Optional[str] = None, + allow: t.Optional[bool] = None + ) -> None: + ... + + @abstractmethod + def get_display_info(self) -> RuleDisplayInfo: + ... + +class RuleStore(Store[RuleRef]): + @abstractmethod + def get_display_infos(self, allow: t.Optional[bool] = None) \ + -> t.Sequence[RuleDisplayInfo]: + ... + + @abstractmethod + def add(self, pattern: str, allow: bool) -> RuleRef: + ... + + @abstractmethod + def get_by_pattern(self, pattern: str) -> RuleRef: + ... + + +class RepoNameInvalid(HaketiloException): + pass + +class RepoNameTaken(HaketiloException): + pass + +class RepoUrlInvalid(HaketiloException): + pass + +class RepoCommunicationError(HaketiloException): + pass + +@dc.dataclass(frozen=True) +class FileInstallationError(HaketiloException): + repo_id: str + sha256: str + +@dc.dataclass(frozen=True) +class FileIntegrityError(FileInstallationError): + invalid_sha256: str + +@dc.dataclass(frozen=True) +class FileMissingError(FileInstallationError): + pass + +class RepoApiVersionUnsupported(HaketiloException): + pass + +@dc.dataclass(frozen=True, unsafe_hash=True) # type: ignore[misc] +class RepoRef(Ref): + """....""" + @abstractmethod + def remove(self) -> None: + """....""" + ... + + @abstractmethod + def update( + self, + *, + name: t.Optional[str] = None, + url: t.Optional[str] = None + ) -> None: + """....""" + ... + + @abstractmethod + def refresh(self) -> None: + """....""" + ... + + @abstractmethod + def get_display_info(self) -> 'RepoDisplayInfo': + ... + +@dc.dataclass(frozen=True) +class RepoDisplayInfo: + ref: RepoRef + is_local_semirepo: bool + name: str + url: str + deleted: bool + last_refreshed: t.Optional[datetime] + resource_count: int + mapping_count: int + +class RepoStore(Store[RepoRef]): + @abstractmethod + def get_display_infos(self, include_deleted: bool = False) -> \ + t.Sequence[RepoDisplayInfo]: + ... + + @abstractmethod + def add(self, name: str, url: str) -> RepoRef: + ... + + +@dc.dataclass(frozen=True, unsafe_hash=True) # type: ignore[misc] +class RepoIterationRef(Ref): + """....""" + pass + + +@dc.dataclass(frozen=True) +class FileData: + mime_type: str + name: str + contents: bytes + + +@dc.dataclass(frozen=True) +class MappingDisplayInfo(item_infos.CorrespondsToMappingDCMixin): + ref: 'MappingRef' + identifier: str + enabled: EnabledStatus + frozen: t.Optional[FrozenStatus] + active_version: t.Optional['MappingVersionDisplayInfo'] + +@dc.dataclass(frozen=True) +class RichMappingDisplayInfo(MappingDisplayInfo): + all_versions: t.Sequence['MappingVersionDisplayInfo'] + +@dc.dataclass(frozen=True) +class MappingVersionDisplayInfo(item_infos.CorrespondsToMappingDCMixin): + ref: 'MappingVersionRef' + info: item_infos.MappingInfo + installed: InstalledStatus + active: ActiveStatus + is_orphan: bool + is_local: bool + +@dc.dataclass(frozen=True, unsafe_hash=True) # type: ignore[misc] +class MappingRef(Ref, item_infos.CorrespondsToMappingDCMixin): + """....""" + @abstractmethod + def update_status( + self, + enabled: EnabledStatus, + frozen: t.Optional[FrozenStatus] = None + ) -> None: + ... + + @abstractmethod + def get_display_info(self) -> RichMappingDisplayInfo: + ... + + +class MappingStore(Store[MappingRef]): + @abstractmethod + def get_display_infos(self) -> t.Sequence[MappingDisplayInfo]: + ... + + @abstractmethod + def get_by_identifier(self, identifier: str) -> MappingRef: + ... + +@dc.dataclass(frozen=True, unsafe_hash=True) # type: ignore[misc] +class MappingVersionRef(Ref, item_infos.CorrespondsToMappingDCMixin): + @abstractmethod + def install(self) -> None: + ... + + @abstractmethod + def uninstall(self) -> t.Optional['MappingVersionRef']: + ... + + @abstractmethod + def ensure_depended_items_installed(self) -> None: + ... + + @abstractmethod + def update_mapping_status( + self, + enabled: EnabledStatus, + frozen: t.Optional[FrozenStatus] = None + ) -> None: + ... + + @abstractmethod + def get_license_file(self, name: str) -> FileData: + ... + + @abstractmethod + def get_upstream_license_file_url(self, name: str) -> str: + ... + + @abstractmethod + def get_required_mapping(self, identifier: str) -> 'MappingVersionRef': + ... + + @abstractmethod + def get_payload_resource(self, pattern: str, identifier: str) \ + -> 'ResourceVersionRef': + ... + + @abstractmethod + def get_item_display_info(self) -> RichMappingDisplayInfo: + ... + +class MappingVersionStore(Store[MappingVersionRef]): + pass + + +@dc.dataclass(frozen=True) +class ResourceDisplayInfo(item_infos.CorrespondsToResourceDCMixin): + ref: 'ResourceRef' + identifier: str + +@dc.dataclass(frozen=True) +class RichResourceDisplayInfo(ResourceDisplayInfo): + all_versions: t.Sequence['ResourceVersionDisplayInfo'] + +@dc.dataclass(frozen=True) +class ResourceVersionDisplayInfo(item_infos.CorrespondsToResourceDCMixin): + ref: 'ResourceVersionRef' + info: item_infos.ResourceInfo + installed: InstalledStatus + active: ActiveStatus + is_orphan: bool + is_local: bool + +@dc.dataclass(frozen=True, unsafe_hash=True) # type: ignore[misc] +class ResourceRef(Ref, item_infos.CorrespondsToResourceDCMixin): + @abstractmethod + def get_display_info(self) -> RichResourceDisplayInfo: + ... + +class ResourceStore(Store[ResourceRef]): + @abstractmethod + def get_display_infos(self) -> t.Sequence[ResourceDisplayInfo]: + ... + + @abstractmethod + def get_by_identifier(self, identifier: str) -> ResourceRef: + ... + + +@dc.dataclass(frozen=True, unsafe_hash=True) # type: ignore[misc] +class ResourceVersionRef(Ref, item_infos.CorrespondsToResourceDCMixin): + @abstractmethod + def install(self) -> None: + ... + + @abstractmethod + def uninstall(self) -> t.Optional['ResourceVersionRef']: + ... + + @abstractmethod + def get_license_file(self, name: str) -> FileData: + ... + + @abstractmethod + def get_resource_file(self, name: str) -> FileData: + ... + + @abstractmethod + def get_upstream_license_file_url(self, name: str) -> str: + ... + + @abstractmethod + def get_upstream_resource_file_url(self, name: str) -> str: + ... + + @abstractmethod + def get_dependency(self, identifier: str) -> 'ResourceVersionRef': + ... + + @abstractmethod + def get_item_display_info(self) -> RichResourceDisplayInfo: + ... + +class ResourceVersionStore(Store[ResourceVersionRef]): + pass + + +@dc.dataclass(frozen=True) +class PayloadKey: + """....""" + ref: 'PayloadRef' + + mapping_identifier: str + + def __lt__(self, other: 'PayloadKey') -> bool: + """....""" + return self.mapping_identifier < other.mapping_identifier + +@dc.dataclass(frozen=True) +class PayloadData: + """....""" + ref: 'PayloadRef' + + explicitly_enabled: bool + unique_token: str + mapping_identifier: str + pattern: str + pattern_path_segments: tuple[str, ...] + eval_allowed: bool + cors_bypass_allowed: bool + global_secret: bytes + +@dc.dataclass(frozen=True) +class PayloadDisplayInfo: + ref: 'PayloadRef' + + mapping_info: MappingVersionDisplayInfo + pattern: str + has_problems: bool + +@dc.dataclass(frozen=True, unsafe_hash=True) # type: ignore[misc] +class PayloadRef(Ref): + """....""" + @abstractmethod + def get_data(self) -> PayloadData: + """....""" + ... + + @abstractmethod + def has_problems(self) -> bool: + ... + + @abstractmethod + def get_display_info(self) -> PayloadDisplayInfo: + ... + + @abstractmethod + def ensure_items_installed(self) -> None: + """....""" + ... + + @abstractmethod + def get_script_paths(self) \ + -> t.Iterable[t.Sequence[str]]: + """....""" + ... + + @abstractmethod + def get_file_data(self, path: t.Sequence[str]) \ + -> t.Optional[FileData]: + """....""" + ... + +class PayloadStore(Store[PayloadRef]): + pass + + +class MappingUseMode(Enum): + """ + AUTO - Apply mappings except for those explicitly disabled. + + WHEN_ENABLED - Only apply mappings explicitly marked as enabled. Don't apply + unmarked nor explicitly disabled mappings. + + QUESTION - Automatically apply mappings that are explicitly enabled. Ask + whether to enable unmarked mappings. Don't apply explicitly disabled + ones. + """ + AUTO = 'A' + WHEN_ENABLED = 'W' + QUESTION = 'Q' + + +class PopupStyle(Enum): + """ + DIALOG - Make popup open inside an iframe on the current page. + + TAB - Make popup open in a new tab. + """ + DIALOG = 'D' + TAB = 'T' + +@dc.dataclass(frozen=True) +class PopupSettings: + # We'll implement button later. + #button_trigger: bool + keyboard_trigger: bool + style: PopupStyle + + @property + def popup_enabled(self) -> bool: + return self.keyboard_trigger #or self.button_trigger + +@dc.dataclass(frozen=True) +class HaketiloGlobalSettings: + """....""" + mapping_use_mode: MappingUseMode + default_allow_scripts: bool + advanced_user: bool + repo_refresh_seconds: int + locale: t.Optional[str] + update_waiting: bool + + default_popup_jsallowed: PopupSettings + default_popup_jsblocked: PopupSettings + default_popup_payloadon: PopupSettings + + +class Logger(ABC): + @abstractmethod + def warn(self, msg: str) -> None: + ... + + +class MissingItemError(ValueError): + """....""" + pass + + +@dc.dataclass(frozen=True) +class OrphanItemsStats: + mappings: int + resources: int + + +class HaketiloState(ABC): + """....""" + @abstractmethod + def import_items(self, malcontent_path: Path) -> None: + ... + + @abstractmethod + def count_orphan_items(self) -> OrphanItemsStats: + ... + + @abstractmethod + def prune_orphan_items(self) -> None: + ... + + @abstractmethod + def rule_store(self) -> RuleStore: + ... + + @abstractmethod + def repo_store(self) -> RepoStore: + """....""" + ... + + @abstractmethod + def mapping_store(self) -> MappingStore: + ... + + @abstractmethod + def mapping_version_store(self) -> MappingVersionStore: + ... + + @abstractmethod + def resource_store(self) -> ResourceStore: + ... + + @abstractmethod + def resource_version_store(self) -> ResourceVersionStore: + ... + + @abstractmethod + def payload_store(self) -> PayloadStore: + ... + + @abstractmethod + def get_secret(self) -> bytes: + ... + + @abstractmethod + def get_settings(self) -> HaketiloGlobalSettings: + """....""" + ... + + @abstractmethod + def update_settings( + self, + *, + mapping_use_mode: t.Optional[MappingUseMode] = None, + default_allow_scripts: t.Optional[bool] = None, + advanced_user: t.Optional[bool] = None, + repo_refresh_seconds: t.Optional[int] = None, + locale: t.Optional[str] = None, + default_popup_settings: t.Mapping[str, PopupSettings] = {} + ) -> None: + ... + + @abstractmethod + def upate_all_items(self) -> None: + ... + + @property + @abstractmethod + def listen_host(self) -> str: + ... + + @property + @abstractmethod + def listen_port(self) -> int: + ... + + @abstractmethod + def launch_browser(self) -> bool: + ... + + @property + @abstractmethod + def logger(self) -> Logger: + ... |