Implement configuration management, drag-and-drop functionality, and logging utilities for WebDrop Bridge

This commit is contained in:
claudi 2026-01-28 11:21:11 +01:00
parent 04ef84cf9a
commit 6bef2f6119
9 changed files with 1154 additions and 0 deletions

View file

@ -0,0 +1,106 @@
"""Qt widget for intercepting drag events and initiating native drag operations."""
from pathlib import Path
from typing import List, Optional
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QDrag, QMimeData
from PySide6.QtWidgets import QWidget
from webdrop_bridge.core.validator import PathValidator, ValidationError
class DragInterceptor(QWidget):
"""Widget that handles drag initiation for file paths.
Intercepts drag events from web content and initiates native Qt drag
operations, allowing files to be dragged from web content to native
applications.
Signals:
drag_started: Emitted when a drag operation begins successfully
drag_failed: Emitted when drag initiation fails
"""
# Signals with string parameters (file paths that were dragged)
drag_started = Signal(list) # List[str] - list of file paths
drag_failed = Signal(str) # str - error message
def __init__(self, parent: Optional[QWidget] = None):
"""Initialize the drag interceptor.
Args:
parent: Parent widget
"""
super().__init__(parent)
self._validator: Optional[PathValidator] = None
def set_validator(self, validator: PathValidator) -> None:
"""Set the path validator for this interceptor.
Args:
validator: PathValidator instance to use for validation
"""
self._validator = validator
def initiate_drag(self, file_paths: List[str]) -> bool:
"""Initiate a native drag operation for the given files.
Args:
file_paths: List of file paths to drag
Returns:
True if drag was successfully initiated, False otherwise
"""
if not file_paths:
self.drag_failed.emit("No files to drag")
return False
if not self._validator:
self.drag_failed.emit("Validator not configured")
return False
# Validate all paths first
validated_paths = []
for path_str in file_paths:
try:
path = Path(path_str)
if self._validator.validate(path):
validated_paths.append(path)
except ValidationError as e:
self.drag_failed.emit(f"Validation failed for {path_str}: {e}")
return False
if not validated_paths:
self.drag_failed.emit("No valid files after validation")
return False
# Create MIME data with file URLs
mime_data = QMimeData()
file_urls = [
path.as_uri() for path in validated_paths
]
mime_data.setUrls([QUrl(url) for url in file_urls])
# Create and execute drag operation
drag = QDrag(self)
drag.setMimeData(mime_data)
# Use default drag pixmap (small icon)
drag.setPixmap(self.grab(self.rect()).scaled(
64, 64, Qt.AspectRatioMode.KeepAspectRatio
))
# Execute drag operation (blocking call)
drop_action = drag.exec(Qt.DropAction.CopyAction)
# Check result
if drop_action == Qt.DropAction.CopyAction:
self.drag_started.emit(validated_paths)
return True
else:
self.drag_failed.emit("Drag operation cancelled or failed")
return False
# Import QUrl here to avoid circular import at module level
from PySide6.QtCore import QUrl # noqa: E402, F401

View file

@ -0,0 +1,97 @@
"""Path validation for secure file operations."""
from pathlib import Path
from typing import List
class ValidationError(Exception):
"""Raised when path validation fails."""
pass
class PathValidator:
"""Validates file paths against security whitelist.
Ensures that only files within allowed root directories can be accessed.
All paths are resolved to absolute form before validation to prevent
directory traversal attacks.
"""
def __init__(self, allowed_roots: List[Path]):
"""Initialize validator with allowed root directories.
Args:
allowed_roots: List of Path objects representing allowed root dirs
Raises:
ValidationError: If any root doesn't exist or isn't a directory
"""
self.allowed_roots = []
for root in allowed_roots:
root_path = Path(root).resolve()
if not root_path.exists():
raise ValidationError(
f"Allowed root '{root}' does not exist"
)
if not root_path.is_dir():
raise ValidationError(
f"Allowed root '{root}' is not a directory"
)
self.allowed_roots.append(root_path)
def validate(self, path: Path) -> bool:
"""Validate that path is within an allowed root directory.
Args:
path: File path to validate
Returns:
True if path is valid and accessible
Raises:
ValidationError: If path fails validation
"""
try:
# Resolve to absolute path (handles symlinks, .., etc)
file_path = Path(path).resolve()
except (OSError, ValueError) as e:
raise ValidationError(f"Cannot resolve path '{path}': {e}") from e
# Check file exists
if not file_path.exists():
raise ValidationError(f"File does not exist: {path}")
# Check it's a regular file (not directory, symlink to dir, etc)
if not file_path.is_file():
raise ValidationError(f"Path is not a regular file: {path}")
# Check path is within an allowed root
for allowed_root in self.allowed_roots:
try:
# This raises ValueError if file_path is not relative to root
file_path.relative_to(allowed_root)
return True
except ValueError:
continue
# Not in any allowed root
raise ValidationError(
f"Path '{file_path}' is not within allowed roots: "
f"{self.allowed_roots}"
)
def is_valid(self, path: Path) -> bool:
"""Check if path is valid without raising exception.
Args:
path: File path to check
Returns:
True if valid, False otherwise
"""
try:
return self.validate(path)
except ValidationError:
return False