Refactor drag handling and update tests
- Renamed `initiate_drag` to `handle_drag` in MainWindow and updated related tests. - Improved drag handling logic to utilize a bridge for starting file drags. - Updated `_on_drag_started` and `_on_drag_failed` methods to match new signatures. - Modified test cases to reflect changes in drag handling and assertions. Enhance path validation and logging - Updated `PathValidator` to log warnings for nonexistent roots instead of raising errors. - Adjusted tests to verify the new behavior of skipping nonexistent roots. Update web application UI and functionality - Changed displayed text for drag items to reflect local paths and Azure Blob Storage URLs. - Added debug logging for drag operations in the web application. - Improved instructions for testing drag and drop functionality. Add configuration documentation and example files - Created `CONFIG_README.md` to provide detailed configuration instructions for WebDrop Bridge. - Added `config.example.json` and `config_test.json` for reference and testing purposes. Implement URL conversion logic - Introduced `URLConverter` class to handle conversion of Azure Blob Storage URLs to local paths. - Added unit tests for URL conversion to ensure correct functionality. Develop download interceptor script - Created `download_interceptor.js` to intercept download-related actions in the web application. - Implemented logging for fetch calls, XMLHttpRequests, and Blob URL creations. Add download test page and related tests - Created `test_download.html` for testing various download scenarios. - Implemented `test_download.py` to verify download path resolution and file construction. - Added `test_url_mappings.py` to ensure URL mappings are loaded correctly. Add unit tests for URL converter - Created `test_url_converter.py` to validate URL conversion logic and mapping behavior.
This commit is contained in:
parent
c9704efc8d
commit
88dc358894
21 changed files with 1870 additions and 432 deletions
|
|
@ -1,13 +1,106 @@
|
|||
"""Restricted web view with URL whitelist enforcement for Kiosk-mode."""
|
||||
|
||||
import fnmatch
|
||||
from typing import List, Optional
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from PySide6.QtCore import QUrl
|
||||
from PySide6.QtCore import QStandardPaths, QUrl
|
||||
from PySide6.QtGui import QDesktopServices
|
||||
from PySide6.QtWebEngineCore import QWebEngineNavigationRequest
|
||||
from PySide6.QtWebEngineCore import QWebEngineNavigationRequest, QWebEnginePage, QWebEngineProfile
|
||||
from PySide6.QtWebEngineWidgets import QWebEngineView
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CustomWebEnginePage(QWebEnginePage):
|
||||
"""Custom page that handles new window requests for downloads."""
|
||||
|
||||
def acceptNavigationRequest(
|
||||
self, url: Union[QUrl, str], nav_type: QWebEnginePage.NavigationType, is_main_frame: bool
|
||||
) -> bool:
|
||||
"""Handle navigation requests, including download links.
|
||||
|
||||
Args:
|
||||
url: Target URL (QUrl or string)
|
||||
nav_type: Type of navigation (link click, form submit, etc.)
|
||||
is_main_frame: Whether this is the main frame
|
||||
|
||||
Returns:
|
||||
True to accept navigation, False to reject
|
||||
"""
|
||||
# Convert to string if QUrl
|
||||
url_str = url.toString() if isinstance(url, QUrl) else url
|
||||
|
||||
# Log all navigation attempts for debugging
|
||||
logger.debug(f"Navigation request: {url_str} (type={nav_type}, main_frame={is_main_frame})")
|
||||
|
||||
# Check if this might be a download (common file extensions)
|
||||
download_extensions = [
|
||||
".pdf",
|
||||
".zip",
|
||||
".rar",
|
||||
".7z",
|
||||
".tar",
|
||||
".gz",
|
||||
".doc",
|
||||
".docx",
|
||||
".xls",
|
||||
".xlsx",
|
||||
".ppt",
|
||||
".pptx",
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".png",
|
||||
".gif",
|
||||
".bmp",
|
||||
".svg",
|
||||
".mp4",
|
||||
".mp3",
|
||||
".avi",
|
||||
".mov",
|
||||
".wav",
|
||||
".exe",
|
||||
".msi",
|
||||
".dmg",
|
||||
".pkg",
|
||||
".csv",
|
||||
".txt",
|
||||
".json",
|
||||
".xml",
|
||||
]
|
||||
|
||||
if any(url_str.lower().endswith(ext) for ext in download_extensions):
|
||||
logger.info(f"🔽 Detected potential download URL: {url_str}")
|
||||
# This will trigger downloadRequested if it's a download
|
||||
|
||||
return super().acceptNavigationRequest(url, nav_type, is_main_frame)
|
||||
|
||||
def createWindow(self, window_type: QWebEnginePage.WebWindowType) -> QWebEnginePage:
|
||||
"""Handle new window requests (target=_blank, window.open, etc.).
|
||||
|
||||
Many downloads are triggered via target="_blank" links.
|
||||
|
||||
Args:
|
||||
window_type: Type of window being created
|
||||
|
||||
Returns:
|
||||
New page instance for the window
|
||||
"""
|
||||
logger.info(f"🪟 New window requested, type: {window_type}")
|
||||
|
||||
# Create a temporary page to handle the download
|
||||
# This page will never be displayed but allows downloads to work
|
||||
download_page = QWebEnginePage(self.profile(), self)
|
||||
|
||||
logger.info("✅ Created temporary page for download/popup")
|
||||
|
||||
# Return the temporary page - it will trigger downloadRequested if it's a download
|
||||
return download_page
|
||||
|
||||
|
||||
class RestrictedWebEngineView(QWebEngineView):
|
||||
"""Web view that enforces URL whitelist for Kiosk-mode security.
|
||||
|
|
@ -27,31 +120,81 @@ class RestrictedWebEngineView(QWebEngineView):
|
|||
super().__init__()
|
||||
self.allowed_urls = allowed_urls or []
|
||||
|
||||
# Create persistent profile for cookie and session storage
|
||||
self.profile = self._create_persistent_profile()
|
||||
|
||||
# Use custom page for better download handling with persistent profile
|
||||
custom_page = CustomWebEnginePage(self.profile, self)
|
||||
self.setPage(custom_page)
|
||||
|
||||
logger.info(
|
||||
"RestrictedWebEngineView initialized with CustomWebEnginePage and persistent profile"
|
||||
)
|
||||
|
||||
# Connect to navigation request handler
|
||||
self.page().navigationRequested.connect(self._on_navigation_requested)
|
||||
|
||||
def _on_navigation_requested(
|
||||
self, request: QWebEngineNavigationRequest
|
||||
) -> None:
|
||||
def _create_persistent_profile(self) -> QWebEngineProfile:
|
||||
"""Create and configure a persistent web engine profile.
|
||||
|
||||
This enables persistent cookies and cache storage, allowing
|
||||
authentication sessions (e.g., Microsoft login) to persist
|
||||
across application restarts.
|
||||
|
||||
Returns:
|
||||
Configured QWebEngineProfile with persistent storage
|
||||
"""
|
||||
# Get application data directory
|
||||
app_data_dir = QStandardPaths.writableLocation(
|
||||
QStandardPaths.StandardLocation.AppDataLocation
|
||||
)
|
||||
|
||||
# Create profile directory path
|
||||
profile_path = Path(app_data_dir) / "WebEngineProfile"
|
||||
profile_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create persistent profile with custom storage location
|
||||
# Using "WebDropBridge" as the profile name
|
||||
profile = QWebEngineProfile("WebDropBridge", self)
|
||||
profile.setPersistentStoragePath(str(profile_path))
|
||||
|
||||
# Configure persistent cookies (critical for authentication)
|
||||
profile.setPersistentCookiesPolicy(
|
||||
QWebEngineProfile.PersistentCookiesPolicy.ForcePersistentCookies
|
||||
)
|
||||
|
||||
# Enable HTTP cache for better performance
|
||||
profile.setHttpCacheType(QWebEngineProfile.HttpCacheType.DiskHttpCache)
|
||||
|
||||
# Set cache size to 100 MB
|
||||
profile.setHttpCacheMaximumSize(100 * 1024 * 1024)
|
||||
|
||||
logger.info(f"Created persistent profile at: {profile_path}")
|
||||
logger.info("Cookies policy: ForcePersistentCookies")
|
||||
logger.info("HTTP cache: DiskHttpCache (100 MB)")
|
||||
|
||||
return profile
|
||||
|
||||
def _on_navigation_requested(self, request: QWebEngineNavigationRequest) -> None:
|
||||
"""Handle navigation requests and enforce URL whitelist.
|
||||
|
||||
Args:
|
||||
request: Navigation request to process
|
||||
"""
|
||||
url = request.url
|
||||
url = request.url()
|
||||
|
||||
# If no restrictions, allow all URLs
|
||||
if not self.allowed_urls:
|
||||
return
|
||||
|
||||
# Check if URL matches whitelist
|
||||
if self._is_url_allowed(url): # type: ignore[operator]
|
||||
if self._is_url_allowed(url):
|
||||
# Allow the navigation (default behavior)
|
||||
return
|
||||
|
||||
# URL not whitelisted - open in system browser
|
||||
request.reject()
|
||||
QDesktopServices.openUrl(url) # type: ignore[operator]
|
||||
QDesktopServices.openUrl(url)
|
||||
|
||||
def _is_url_allowed(self, url: QUrl) -> bool:
|
||||
"""Check if a URL matches the whitelist.
|
||||
|
|
@ -98,4 +241,3 @@ class RestrictedWebEngineView(QWebEngineView):
|
|||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue