Commit 5fa68721 authored by nextime's avatar nextime

Split browser code

parent 047b754f
# -*- coding: utf-8 -*-
import os
import sys
import subprocess
import tempfile
import json
import shutil
import asyncio
import platform
import signal
from pathlib import Path
from typing import Dict, List, Optional, Union, Callable, Any
import configparser
from functools import partial
# Import our RuntimeBridge for chrome.runtime API emulation
from assets.browser.js.runtime_bridge import RuntimeBridge, create_runtime_api_script
import re
from PyQt6.QtCore import Qt, QUrl, QProcess, pyqtSignal, QSize, QRect, QTimer, QMimeData, QPoint, QByteArray, QBuffer
from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtGui import QIcon, QAction, QKeySequence, QFont, QColor, QPainter, QDrag, QPixmap
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QTabWidget, QToolBar, QLineEdit,
QPushButton, QWidget, QVBoxLayout, QHBoxLayout, QDialog,
QLabel, QListWidget, QListWidgetItem, QCheckBox, QMenu,
QSizePolicy, QStyle, QFrame, QSplitter, QMessageBox, QStatusBar, QTabBar, QStyleOptionTab, QFileDialog,
QInputDialog, QTextEdit
)
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWebEngineCore import (
QWebEngineProfile, QWebEngineSettings, QWebEnginePage, QWebEngineUrlSchemeHandler,
QWebEngineUrlRequestJob, QWebEngineUrlRequestInterceptor, QWebEngineUrlScheme,
QWebEngineScript
)
import mimetypes
# Import Playwright for API compatibility
from playwright.async_api import async_playwright, Browser as PlaywrightBrowser
from playwright.async_api import Page as PlaywrightPage
from playwright.async_api import BrowserContext as PlaywrightBrowserContext
from page import ChromeWebEnginePage
from tabs import BrowserTab, DetachableTabBar
from injector import ContentScriptInjector
from chrome import ChromeUrlInterceptor
from extensions import ExtensionSchemeHandler, ExtensionDialog
from profiles import ProfileDialog
# Register URL schemes before any QApplication is created
def register_url_schemes():
# Register qextension:// scheme
qextension_scheme = QWebEngineUrlScheme(b"qextension")
qextension_scheme.setFlags(QWebEngineUrlScheme.Flag.SecureScheme |
QWebEngineUrlScheme.Flag.LocalScheme |
QWebEngineUrlScheme.Flag.LocalAccessAllowed |
QWebEngineUrlScheme.Flag.ServiceWorkersAllowed)
QWebEngineUrlScheme.registerScheme(qextension_scheme)
# Register chrome:// scheme
chrome_scheme = QWebEngineUrlScheme(b"chrome")
chrome_scheme.setFlags(QWebEngineUrlScheme.Flag.SecureScheme |
QWebEngineUrlScheme.Flag.LocalScheme |
QWebEngineUrlScheme.Flag.LocalAccessAllowed |
QWebEngineUrlScheme.Flag.ServiceWorkersAllowed)
QWebEngineUrlScheme.registerScheme(chrome_scheme)
print("URL schemes registered")
class Browser(QMainWindow):
"""
Main browser window that wraps the Playwright browser class.
"""
instances = []
def __init__(self, initial_url=None, debug=False, detached_tab=None, profile_path=None):
super().__init__()
Browser.instances.append(self)
self.setWindowTitle("SHMCamStudio Browser")
self.setWindowIcon(QIcon('assets/logo.jpg'))
self.resize(1024, 768)
# Create a central widget and layout
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
self.layout = QVBoxLayout(self.central_widget)
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setSpacing(0)
# Store loaded extensions
self.loaded_extensions = {}
# Create toolbar
self.toolbar = QToolBar()
self.toolbar.setMovable(False)
self.toolbar.setIconSize(QSize(24, 24))
# Add back button
self.back_button = QPushButton()
self.back_button.setIcon(self._get_themed_icon(QStyle.StandardPixmap.SP_ArrowBack))
self.back_button.setToolTip("Back")
self.back_button.clicked.connect(self.navigate_back)
self.toolbar.addWidget(self.back_button)
# Add forward button
self.forward_button = QPushButton()
self.forward_button.setIcon(self._get_themed_icon(QStyle.StandardPixmap.SP_ArrowForward))
self.forward_button.setToolTip("Forward")
self.forward_button.clicked.connect(self.navigate_forward)
self.toolbar.addWidget(self.forward_button)
# Add reload button
self.reload_button = QPushButton()
self.reload_button.setIcon(self._get_themed_icon(QStyle.StandardPixmap.SP_BrowserReload))
self.reload_button.setToolTip("Reload (F5)")
self.reload_button.clicked.connect(self.reload_page)
self.toolbar.addWidget(self.reload_button)
# Add home button
self.home_button = QPushButton()
self.home_button.setIcon(self._get_themed_icon(QStyle.StandardPixmap.SP_DirHomeIcon))
self.home_button.setToolTip("Home")
self.home_button.clicked.connect(self.navigate_home)
self.toolbar.addWidget(self.home_button)
# Add URL input
self.url_input = QLineEdit()
self.url_input.setPlaceholderText("Enter URL...")
self.url_input.returnPressed.connect(self.navigate_to_url)
self.toolbar.addWidget(self.url_input)
# The extension buttons will be inserted before this one, so we need its action.
self.extensions_button = QPushButton()
self.extensions_button.setIcon(self._get_themed_icon(QStyle.StandardPixmap.SP_FileDialogDetailedView))
self.extensions_button.setToolTip("Extensions")
self.extensions_button.clicked.connect(self.show_extensions)
self.extensions_button_action = self.toolbar.addWidget(self.extensions_button)
"""
# Add Chrome URLs button with dropdown menu
self.chrome_button = QPushButton()
self.chrome_button.setIcon(self._get_themed_icon(QStyle.StandardPixmap.SP_TitleBarMenuButton))
self.chrome_button.setToolTip("Chrome URLs")
self.chrome_menu = QMenu(self)
# Add common chrome:// URLs to the menu
chrome_urls = {
"Extensions": "chrome://extensions/",
"Settings": "chrome://settings/",
"History": "chrome://history/",
"Bookmarks": "chrome://bookmarks/",
"Downloads": "chrome://downloads/",
"Flags": "chrome://flags/",
"Version": "chrome://version/",
"GPU": "chrome://gpu/",
"Network": "chrome://net-internals/",
"Inspect": "chrome://inspect/",
"Media": "chrome://media-internals/",
"Components": "chrome://components/",
"System": "chrome://system/",
"Chrome URLs": "chrome://chrome-urls/" # This shows all available chrome:// URLs
}
# Add a separator and debug options
self.chrome_menu.addSeparator()
debug_action = self.chrome_menu.addAction("Debug Chrome URLs")
debug_action.triggered.connect(self.debug_chrome_urls)
for name, url in chrome_urls.items():
action = self.chrome_menu.addAction(name)
action.triggered.connect(lambda checked, u=url: self.navigate_to_chrome_url(u))
self.chrome_button.setMenu(self.chrome_menu)
self.toolbar.addWidget(self.chrome_button)
"""
# Add new window button
self.new_window_button = QPushButton()
self.new_window_button.setIcon(self._get_themed_icon(QStyle.StandardPixmap.SP_FileIcon))
self.new_window_button.setToolTip("New Window (Ctrl+W)")
self.new_window_button.clicked.connect(self.open_new_window)
self.toolbar.addWidget(self.new_window_button)
if debug:
self.debug_button = QPushButton()
self.debug_button.setIcon(self._get_themed_icon(QStyle.StandardPixmap.SP_MessageBoxWarning))
self.debug_button.setToolTip("Toggle Developer Tools")
self.debug_button.clicked.connect(self.toggle_devtools)
self.toolbar.addWidget(self.debug_button)
# Add a test extension button to the toolbar
self.test_extension_button = QPushButton("Test Extension")
self.test_extension_button.setToolTip("Launch Test Extension")
self.test_extension_button.clicked.connect(self.launch_test_extension)
self.toolbar.addWidget(self.test_extension_button)
self.layout.addWidget(self.toolbar)
# Create tab widget
self.tabs = QTabWidget()
self.tab_bar = DetachableTabBar()
self.tabs.setTabBar(self.tab_bar)
self.tab_bar.tabDetached.connect(self.handle_tab_detached)
self.tabs.setTabsClosable(True)
# self.tabs.setMovable(True) # This is handled by our DetachableTabBar
self.tabs.setDocumentMode(True)
self.tabs.tabCloseRequested.connect(self.close_tab)
self.tabs.currentChanged.connect(self.tab_changed)
self.layout.addWidget(self.tabs)
# Add status bar
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
# Add a new tab button
self.tabs.setCornerWidget(self._create_new_tab_button(), Qt.Corner.TopRightCorner)
# Set the home URL from config
config = configparser.ConfigParser()
config.read('shmcamstudio.conf')
self.home_url = config.get('Browser', 'home_url', fallback='https://www.google.com')
# Centralized browser profile
if not profile_path:
# This should not happen if main is used, but as a fallback
profile_path = "data/browser_profile_default"
self.profile_dir = profile_path
if not os.path.exists(self.profile_dir):
os.makedirs(self.profile_dir)
# Extensions must be in a specific subdirectory of the profile
self.extensions_dir = os.path.join(self.profile_dir, "Default", "Extensions")
if not os.path.exists(self.extensions_dir):
os.makedirs(self.extensions_dir)
self.profile = QWebEngineProfile(self.profile_dir, self)
# Enable extension developer mode to allow loading unpacked extensions
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.AllowWindowActivationFromJavaScript, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.JavascriptCanAccessClipboard, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.AllowRunningInsecureContent, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.JavascriptCanPaste, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.JavascriptCanOpenWindows, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.LocalStorageEnabled, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.WebGLEnabled, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.PluginsEnabled, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.FullScreenSupportEnabled, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.ErrorPageEnabled, False)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.FocusOnNavigationEnabled, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, True)
# Enable access to chrome:// URLs and other special URLs
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, True)
# Additional settings to ensure chrome:// URLs work
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.ErrorPageEnabled, False)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.FocusOnNavigationEnabled, True)
# Set developer mode flag in preferences
self.profile.setHttpUserAgent(self.profile.httpUserAgent() + " ChromiumExtensionDevMode")
# Enable Autofill feature for QWebEngine (using preferences instead of WebAttribute)
# Note: AutoFillEnabled attribute is not available in this PyQt6 version
# Create a directory for extension preferences if it doesn't exist
prefs_dir = os.path.join(self.profile_dir, "Default")
if not os.path.exists(prefs_dir):
os.makedirs(prefs_dir)
# Create or update the Preferences file to enable extension developer mode
prefs_file = os.path.join(prefs_dir, "Preferences")
prefs_data = {}
if os.path.exists(prefs_file):
try:
with open(prefs_file, 'r') as f:
prefs_data = json.load(f)
except json.JSONDecodeError:
# If the file exists but is invalid JSON, start with an empty dict
pass
# Set extension developer mode preferences
if 'extensions' not in prefs_data:
prefs_data['extensions'] = {}
prefs_data['extensions']['developer_mode'] = True
# Enable unpacked extensions and user scripts
if 'extensions' in prefs_data:
# Allow loading unpacked extensions
prefs_data['extensions']['allow_file_access'] = True
# Allow user scripts (required by some extensions like Lovense)
prefs_data['extensions']['allow_user_scripts'] = True
# Enable Autofill in preferences
if 'autofill' not in prefs_data:
prefs_data['autofill'] = {}
prefs_data['autofill']['enabled'] = True
# Enable additional features for extensions
if 'browser' not in prefs_data:
prefs_data['browser'] = {}
if 'enabled_labs_experiments' not in prefs_data['browser']:
prefs_data['browser']['enabled_labs_experiments'] = []
# Add Autofill experiments if not already present
autofill_experiments = [
"enable-autofill-credit-card-upload",
"enable-autofill-credit-card-authentication",
"enable-autofill-address-save-prompts",
"enable-experimental-web-platform-features"
]
for experiment in autofill_experiments:
if experiment not in prefs_data['browser']['enabled_labs_experiments']:
prefs_data['browser']['enabled_labs_experiments'].append(experiment)
# Write the updated preferences back to the file
with open(prefs_file, 'w') as f:
json.dump(prefs_data, f, indent=2)
# Check for Lovense extension and configure it specifically
lovense_dir = os.path.join(self.extensions_dir, "lovense")
if os.path.exists(lovense_dir):
print("Lovense extension found, applying special configuration...")
# Create secure preferences file if it doesn't exist
secure_prefs_file = os.path.join(self.profile_dir, "Default", "Secure Preferences")
secure_prefs_data = {}
if os.path.exists(secure_prefs_file):
try:
with open(secure_prefs_file, 'r') as f:
secure_prefs_data = json.load(f)
except:
secure_prefs_data = {}
# Ensure extensions section exists
if 'extensions' not in secure_prefs_data:
secure_prefs_data['extensions'] = {}
# Add settings for Lovense
if 'settings' not in secure_prefs_data['extensions']:
secure_prefs_data['extensions']['settings'] = {}
# Set Lovense as trusted
secure_prefs_data['extensions']['settings']["lovense"] = {
"ack_external_install_prompt": True,
"installed_by_default": False,
"installed_by_oem": False,
"installed_by_policy": False,
"mark_acknowledged_external_install": True,
"was_installed_by_enterprise_policy": False
}
# Write secure preferences
with open(secure_prefs_file, 'w') as f:
json.dump(secure_prefs_data, f, indent=2)
# Create Local State file with extension settings
local_state_file = os.path.join(self.profile_dir, "Local State")
local_state_data = {}
if os.path.exists(local_state_file):
try:
with open(local_state_file, 'r') as f:
local_state_data = json.load(f)
except:
local_state_data = {}
# Ensure extensions section exists
if 'extensions' not in local_state_data:
local_state_data['extensions'] = {}
# Enable developer mode in local state
local_state_data['extensions']['ui'] = {
"developer_mode": True
}
# Write local state
with open(local_state_file, 'w') as f:
json.dump(local_state_data, f, indent=2)
print("Lovense extension configuration complete")
# Update loaded extensions list
self.update_extensions_list()
# Install URL scheme handlers
self.extension_scheme_handler = ExtensionSchemeHandler(self.extensions_dir, self.loaded_extensions)
# Reinstall URL scheme handlers
self.profile.installUrlSchemeHandler(b"qextension", self.extension_scheme_handler)
# Re-register the RuntimeBridge with the QWebChannel
self.web_channel = QWebChannel(self)
self.runtime_bridge = RuntimeBridge(self, self.extensions_dir, self)
self.web_channel.registerObject("runtimeBridge", self.runtime_bridge)
# Initialize the content script injector
self.content_script_injector = ContentScriptInjector(self.extensions_dir)
# Register chrome:// protocol interceptor
QWebEngineProfile.defaultProfile().setUrlRequestInterceptor(
ChromeUrlInterceptor(self)
)
# Add the chrome.runtime API script to the profile's scripts
self.runtime_api_script = create_runtime_api_script()
self.profile.scripts().insert(self.runtime_api_script)
if detached_tab:
widget, title = detached_tab
index = self.tabs.addTab(widget, title)
self.tabs.setCurrentIndex(index)
widget.browser = self
widget.urlChanged.connect(self.update_url)
widget.titleChanged.connect(lambda t, tab=widget: self.update_title(t, tab))
widget.loadStatusChanged.connect(self.update_status_bar)
if hasattr(widget, 'web_view'):
self.update_title(widget.web_view.title(), widget)
self.update_url(widget.web_view.url().toString())
else:
# Create a new tab on startup
url_to_open = initial_url if initial_url is not None else self.home_url
self.new_page(url_to_open)
# Set up keyboard shortcuts
self._setup_shortcuts()
# Store Playwright objects
self.playwright = None
self.playwright_browser = None
self.browser_contexts = {}
self.dev_tools_windows = []
self.open_extension_popups = []
self.extension_actions = []
self.update_extension_buttons()
def closeEvent(self, event):
if self in Browser.instances:
Browser.instances.remove(self)
super().closeEvent(event)
def _get_themed_icon(self, standard_pixmap, color="white"):
"""Create a themed icon from a standard pixmap."""
icon = self.style().standardIcon(standard_pixmap)
pixmap = icon.pixmap(self.toolbar.iconSize())
painter = QPainter(pixmap)
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceIn)
painter.fillRect(pixmap.rect(), QColor(color))
painter.end()
return QIcon(pixmap)
def _create_new_tab_button(self):
"""Create a button for adding new tabs."""
button = QPushButton("+")
button.setToolTip("New Tab")
button.clicked.connect(lambda: self.new_page(self.home_url))
return button
def _setup_shortcuts(self):
"""Set up keyboard shortcuts."""
# Ctrl+T: New Tab
new_tab_shortcut = QAction("New Tab", self)
new_tab_shortcut.setShortcut(QKeySequence("Ctrl+T"))
new_tab_shortcut.triggered.connect(lambda: self.new_page(self.home_url))
self.addAction(new_tab_shortcut)
# Ctrl+Tab: Cycle Tabs
cycle_tabs_shortcut = QAction("Cycle Tabs", self)
cycle_tabs_shortcut.setShortcut(QKeySequence("Ctrl+Tab"))
cycle_tabs_shortcut.triggered.connect(self.cycle_tabs)
self.addAction(cycle_tabs_shortcut)
# Ctrl+W: New Window
new_window_shortcut = QAction("New Window", self)
new_window_shortcut.setShortcut(QKeySequence("Ctrl+W"))
new_window_shortcut.triggered.connect(self.open_new_window)
self.addAction(new_window_shortcut)
# Ctrl+L: Focus URL bar
focus_url_shortcut = QAction("Focus URL", self)
focus_url_shortcut.setShortcut(QKeySequence("Ctrl+L"))
focus_url_shortcut.triggered.connect(self.url_input.setFocus)
self.addAction(focus_url_shortcut)
# F5: Reload page
reload_shortcut = QAction("Reload", self)
reload_shortcut.setShortcut(QKeySequence("F5"))
reload_shortcut.triggered.connect(self.reload_page)
self.addAction(reload_shortcut)
def cycle_tabs(self):
"""Cycle through the open tabs."""
if self.tabs.count() > 1:
next_index = (self.tabs.currentIndex() + 1) % self.tabs.count()
self.tabs.setCurrentIndex(next_index)
def on_tab_removed(self):
"""Close the window if the last tab is removed."""
if self.tabs.count() == 0:
self.close()
def handle_tab_detached(self, index, pos):
"""Handle a tab being detached to a new window."""
if index < 0 or index >= self.tabs.count():
return
widget = self.tabs.widget(index)
title = self.tabs.tabText(index)
try:
widget.urlChanged.disconnect(self.update_url)
widget.titleChanged.disconnect()
widget.loadStatusChanged.disconnect(self.update_status_bar)
except (TypeError, RuntimeError):
pass
# Create a new browser instance, which will take ownership of the widget.
new_browser = Browser(
debug=("--debug" in sys.argv),
detached_tab=(widget, title),
profile_path=self.profile_dir
)
new_browser.setGeometry(self.geometry())
new_browser.move(pos - QPoint(new_browser.width() // 4, 10))
new_browser.show()
# Now that the widget is safely reparented, remove the tab from the old browser.
# The on_tab_removed method will handle closing the window if it's empty.
self.tabs.removeTab(index)
self.on_tab_removed()
def new_page(self, url="about:blank"):
"""Create a new browser tab with the specified URL."""
# Create a new tab
tab = BrowserTab(parent=self.tabs, url=url, profile=self.profile)
# Store a reference to the Browser instance
tab.browser = self
tab.urlChanged.connect(self.update_url)
tab.titleChanged.connect(lambda title, tab=tab: self.update_title(title, tab))
tab.loadStatusChanged.connect(self.update_status_bar)
# Add the tab to the tab widget
index = self.tabs.addTab(tab, "New Tab")
self.tabs.setCurrentIndex(index)
return tab
def close_tab(self, index):
"""Close the tab at the specified index."""
if index < 0 or index >= self.tabs.count():
return
# Get the tab widget and schedule it for deletion.
tab = self.tabs.widget(index)
if tab:
tab.deleteLater()
# Remove the tab from the tab bar.
self.tabs.removeTab(index)
self.on_tab_removed()
def tab_changed(self, index):
"""Handle tab change events."""
if index < 0:
return
# Update the URL input
tab = self.tabs.widget(index)
if hasattr(tab, 'url'):
self.url_input.setText(tab.url)
self.update_status_bar()
def update_url(self, url):
"""Update the URL input when the page URL changes."""
self.url_input.setText(url)
# Update the tab's stored URL
current_tab = self.tabs.currentWidget()
if current_tab:
current_tab.url = url
def update_title(self, title, tab):
"""Update the tab title when the page title changes."""
index = self.tabs.indexOf(tab)
if index >= 0:
self.tabs.setTabText(index, title or "New Tab")
def navigate_to_url(self):
"""Navigate to the URL in the URL input."""
url = self.url_input.text().strip()
# Add http:// if no protocol is specified
if url and not url.startswith(("http://", "https://", "file://", "about:", "chrome://", "qextension://", "qrc://")):
url = "http://" + url
# Navigate the current tab
current_tab = self.tabs.currentWidget()
if current_tab:
current_tab.navigate(url)
def navigate_home(self):
"""Navigate to the home URL."""
current_tab = self.tabs.currentWidget()
if current_tab:
current_tab.navigate(self.home_url)
def navigate_to_chrome_url(self, url):
"""Navigate to a chrome:// URL."""
print(f"Navigating to chrome URL: {url}")
# Create a new tab specifically for chrome:// URLs
tab = self.new_page("about:blank")
# Ensure the tab is using our custom ChromeWebEnginePage
if hasattr(tab, 'web_view'):
# If the page isn't already a ChromeWebEnginePage, replace it
if not isinstance(tab.web_view.page(), ChromeWebEnginePage):
page = ChromeWebEnginePage(self.profile, tab.web_view)
tab.web_view.setPage(page)
# Now navigate to the chrome:// URL
tab.web_view.load(QUrl(url))
print(f"Created new tab for chrome URL: {url}")
def navigate_back(self):
"""Navigate back in the current tab."""
current_tab = self.tabs.currentWidget()
if current_tab and hasattr(current_tab, 'web_view'):
current_tab.web_view.back()
def navigate_forward(self):
"""Navigate forward in the current tab."""
current_tab = self.tabs.currentWidget()
if current_tab and hasattr(current_tab, 'web_view'):
current_tab.web_view.forward()
def reload_page(self):
"""Reload the current tab."""
current_tab = self.tabs.currentWidget()
if current_tab and hasattr(current_tab, 'web_view'):
current_tab.web_view.reload()
def open_new_window(self):
"""Open a new browser window."""
new_browser = Browser(debug="--debug" in sys.argv, profile_path=self.profile_dir)
new_browser.show()
def toggle_devtools(self):
"""Opens the remote developer tools URL in a new window or shows extension debug info."""
# Create a dialog to show debug options
debug_dialog = QDialog(self)
debug_dialog.setWindowTitle("Developer Tools")
debug_dialog.resize(1024, 768)
layout = QVBoxLayout()
# Add tabs for different debug options
tabs = QTabWidget()
# Tab 1: Remote DevTools
remote_tab = QWidget()
remote_layout = QVBoxLayout(remote_tab)
web_view = QWebEngineView()
web_view.load(QUrl("http://localhost:9222"))
remote_layout.addWidget(web_view)
# Tab 2: Extension Debug Info
ext_tab = QWidget()
ext_layout = QVBoxLayout(ext_tab)
# Add a text area to show preferences file content
prefs_label = QLabel("Preferences File Content:")
ext_layout.addWidget(prefs_label)
prefs_text = QTextEdit()
prefs_text.setReadOnly(True)
ext_layout.addWidget(prefs_text)
# Add a button to refresh the preferences content
refresh_button = QPushButton("Refresh Preferences")
ext_layout.addWidget(refresh_button)
# Add a button to force enable developer mode
force_button = QPushButton("Force Enable Developer Mode")
ext_layout.addWidget(force_button)
# Add a button specifically for fixing the Lovense extension
lovense_button = QPushButton("Fix Lovense Extension")
ext_layout.addWidget(lovense_button)
# Function to load and display preferences
def load_preferences():
prefs_file = os.path.join(self.profile_dir, "Default", "Preferences")
if os.path.exists(prefs_file):
try:
with open(prefs_file, 'r') as f:
prefs_data = json.load(f)
# Format the JSON for better readability
formatted_json = json.dumps(prefs_data, indent=2)
prefs_text.setText(formatted_json)
# Highlight extension developer mode status
ext_dev_mode = prefs_data.get('extensions', {}).get('developer_mode', False)
allow_file_access = prefs_data.get('extensions', {}).get('allow_file_access', False)
allow_user_scripts = prefs_data.get('extensions', {}).get('allow_user_scripts', False)
status_text = f"Extension Developer Mode: {ext_dev_mode}\n"
status_text += f"Allow File Access: {allow_file_access}\n"
status_text += f"Allow User Scripts: {allow_user_scripts}\n"
QMessageBox.information(debug_dialog, "Extension Settings Status", status_text)
except Exception as e:
prefs_text.setText(f"Error loading preferences: {e}")
else:
prefs_text.setText("Preferences file not found")
# Function to force enable developer mode
def force_developer_mode():
prefs_file = os.path.join(self.profile_dir, "Default", "Preferences")
if os.path.exists(prefs_file):
try:
with open(prefs_file, 'r') as f:
prefs_data = json.load(f)
# Ensure extensions section exists
if 'extensions' not in prefs_data:
prefs_data['extensions'] = {}
# Force enable all developer mode settings
prefs_data['extensions']['developer_mode'] = True
prefs_data['extensions']['allow_file_access'] = True
prefs_data['extensions']['allow_user_scripts'] = True
# Write back to file
with open(prefs_file, 'w') as f:
json.dump(prefs_data, f, indent=2)
# Reload the profile
self.reload_profile_and_tabs()
QMessageBox.information(debug_dialog, "Success",
"Developer mode forcefully enabled. Profile reloaded.")
# Refresh the displayed preferences
load_preferences()
except Exception as e:
QMessageBox.critical(debug_dialog, "Error", f"Could not force developer mode: {e}")
else:
QMessageBox.warning(debug_dialog, "Error", "Preferences file not found")
# Function to specifically fix the Lovense extension
def fix_lovense_extension():
# Check if Lovense extension exists
lovense_dir = os.path.join(self.extensions_dir, "lovense")
if not os.path.exists(lovense_dir):
QMessageBox.warning(debug_dialog, "Error", "Lovense extension not found in extensions directory")
return
try:
# 1. Update preferences file with specific settings for Lovense
prefs_file = os.path.join(self.profile_dir, "Default", "Preferences")
if os.path.exists(prefs_file):
with open(prefs_file, 'r') as f:
prefs_data = json.load(f)
# Ensure extensions section exists
if 'extensions' not in prefs_data:
prefs_data['extensions'] = {}
# Force enable all developer mode settings
prefs_data['extensions']['developer_mode'] = True
prefs_data['extensions']['allow_file_access'] = True
prefs_data['extensions']['allow_user_scripts'] = True
# Add specific settings for Lovense extension
if 'settings' not in prefs_data['extensions']:
prefs_data['extensions']['settings'] = {}
# Get Lovense extension ID (directory name)
lovense_id = "lovense"
# Set specific permissions for Lovense
prefs_data['extensions']['settings'][lovense_id] = {
"active_permissions": {
"api": ["storage", "unlimitedStorage", "userScripts", "desktopCapture"],
"explicit_host": ["<all_urls>"],
"manifest_permissions": [],
"scriptable_host": ["<all_urls>"]
},
"granted_permissions": {
"api": ["storage", "unlimitedStorage", "userScripts", "desktopCapture"],
"explicit_host": ["<all_urls>"],
"manifest_permissions": [],
"scriptable_host": ["<all_urls>"]
},
"location": 1, # 1 = unpacked extension
"runtime_allowed_hosts": ["*://*.lovense.com/*", "*://localhost/*"],
"runtime_blocked_hosts": []
}
# Write back to file
with open(prefs_file, 'w') as f:
json.dump(prefs_data, f, indent=2)
# 2. Create a secure_preferences file if it doesn't exist
secure_prefs_file = os.path.join(self.profile_dir, "Default", "Secure Preferences")
secure_prefs_data = {}
if os.path.exists(secure_prefs_file):
try:
with open(secure_prefs_file, 'r') as f:
secure_prefs_data = json.load(f)
except:
secure_prefs_data = {}
# Ensure extensions section exists
if 'extensions' not in secure_prefs_data:
secure_prefs_data['extensions'] = {}
# Add settings for Lovense
if 'settings' not in secure_prefs_data['extensions']:
secure_prefs_data['extensions']['settings'] = {}
# Set Lovense as trusted
secure_prefs_data['extensions']['settings']["lovense"] = {
"ack_external_install_prompt": True,
"installed_by_default": False,
"installed_by_oem": False,
"installed_by_policy": False,
"mark_acknowledged_external_install": True,
"was_installed_by_enterprise_policy": False
}
# Write secure preferences
with open(secure_prefs_file, 'w') as f:
json.dump(secure_prefs_data, f, indent=2)
# 3. Create Local State file with extension settings
local_state_file = os.path.join(self.profile_dir, "Local State")
local_state_data = {}
if os.path.exists(local_state_file):
try:
with open(local_state_file, 'r') as f:
local_state_data = json.load(f)
except:
local_state_data = {}
# Ensure extensions section exists
if 'extensions' not in local_state_data:
local_state_data['extensions'] = {}
# Enable developer mode in local state
local_state_data['extensions']['ui'] = {
"developer_mode": True
}
# Write local state
with open(local_state_file, 'w') as f:
json.dump(local_state_data, f, indent=2)
# Reload the profile
self.reload_profile_and_tabs()
QMessageBox.information(debug_dialog, "Success",
"Lovense extension has been fixed and profile reloaded.")
# Refresh the displayed preferences
load_preferences()
except Exception as e:
QMessageBox.critical(debug_dialog, "Error", f"Could not fix Lovense extension: {e}")
# Connect buttons to functions
refresh_button.clicked.connect(load_preferences)
force_button.clicked.connect(force_developer_mode)
lovense_button.clicked.connect(fix_lovense_extension)
# Load preferences initially
load_preferences()
# Add tabs to the tab widget
tabs.addTab(remote_tab, "Remote DevTools")
tabs.addTab(ext_tab, "Extension Debug")
layout.addWidget(tabs)
debug_dialog.setLayout(layout)
debug_dialog.show()
self.dev_tools_windows.append(debug_dialog)
debug_dialog.finished.connect(lambda: self.dev_tools_windows.remove(debug_dialog))
def update_status_bar(self):
"""Update the status bar based on the current tab's state."""
current_tab = self.tabs.currentWidget()
if not current_tab:
self.status_bar.showMessage("Ready")
return
if current_tab.is_loading:
self.status_bar.showMessage("Loading...")
else:
self.status_bar.showMessage("Ready")
def reload_profile_and_tabs(self):
"""Reloads the browser profile and recreates pages in all tabs."""
print("Reloading profile to apply extension changes...")
# 1. Save the URLs and current index of all open tabs
urls = []
current_index = self.tabs.currentIndex()
for i in range(self.tabs.count()):
tab = self.tabs.widget(i)
if hasattr(tab, 'web_view') and tab.web_view.url().isValid():
urls.append(tab.web_view.url().toString())
else:
urls.append("about:blank")
# 2. Store the old profile to delete it later
old_profile = self.profile
old_profile.clearHttpCache()
# 3. Clear all existing tabs. This removes references to pages using the old profile.
self.tabs.clear()
# 4. Create a new profile object. This forces a re-read from disk because
# the old one will be properly deleted.
self.profile = QWebEngineProfile(self.profile_dir, self)
# Re-enable extension developer mode and Autofill settings
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.AllowWindowActivationFromJavaScript, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.JavascriptCanAccessClipboard, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.AllowRunningInsecureContent, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.JavascriptCanPaste, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.JavascriptCanOpenWindows, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.LocalStorageEnabled, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.WebGLEnabled, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.PluginsEnabled, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.FullScreenSupportEnabled, True)
# Set developer mode flag in preferences
self.profile.setHttpUserAgent(self.profile.httpUserAgent() + " ChromiumExtensionDevMode")
# Re-apply extension developer mode settings to preferences file
prefs_dir = os.path.join(self.profile_dir, "Default")
prefs_file = os.path.join(prefs_dir, "Preferences")
if os.path.exists(prefs_file):
try:
with open(prefs_file, 'r') as f:
prefs_data = json.load(f)
# Ensure extension developer mode is enabled
if 'extensions' not in prefs_data:
prefs_data['extensions'] = {}
prefs_data['extensions']['developer_mode'] = True
prefs_data['extensions']['allow_file_access'] = True
prefs_data['extensions']['allow_user_scripts'] = True
# Write the updated preferences back to the file
with open(prefs_file, 'w') as f:
json.dump(prefs_data, f, indent=2)
# Check for Lovense extension and reconfigure it
lovense_dir = os.path.join(self.extensions_dir, "lovense")
if os.path.exists(lovense_dir):
print("Lovense extension found during reload, reapplying special configuration...")
# Update specific settings for Lovense extension in preferences
if 'settings' not in prefs_data['extensions']:
prefs_data['extensions']['settings'] = {}
# Set specific permissions for Lovense
prefs_data['extensions']['settings']["lovense"] = {
"active_permissions": {
"api": ["storage", "unlimitedStorage", "userScripts", "desktopCapture"],
"explicit_host": ["<all_urls>"],
"manifest_permissions": [],
"scriptable_host": ["<all_urls>"]
},
"granted_permissions": {
"api": ["storage", "unlimitedStorage", "userScripts", "desktopCapture"],
"explicit_host": ["<all_urls>"],
"manifest_permissions": [],
"scriptable_host": ["<all_urls>"]
},
"location": 1, # 1 = unpacked extension
"runtime_allowed_hosts": ["*://*.lovense.com/*", "*://localhost/*"],
"runtime_blocked_hosts": []
}
# Write updated preferences with Lovense settings
with open(prefs_file, 'w') as f:
json.dump(prefs_data, f, indent=2)
# Update secure preferences
secure_prefs_file = os.path.join(self.profile_dir, "Default", "Secure Preferences")
secure_prefs_data = {}
if os.path.exists(secure_prefs_file):
try:
with open(secure_prefs_file, 'r') as f:
secure_prefs_data = json.load(f)
except:
secure_prefs_data = {}
# Ensure extensions section exists
if 'extensions' not in secure_prefs_data:
secure_prefs_data['extensions'] = {}
# Add settings for Lovense
if 'settings' not in secure_prefs_data['extensions']:
secure_prefs_data['extensions']['settings'] = {}
# Set Lovense as trusted
secure_prefs_data['extensions']['settings']["lovense"] = {
"ack_external_install_prompt": True,
"installed_by_default": False,
"installed_by_oem": False,
"installed_by_policy": False,
"mark_acknowledged_external_install": True,
"was_installed_by_enterprise_policy": False
}
# Write secure preferences
with open(secure_prefs_file, 'w') as f:
json.dump(secure_prefs_data, f, indent=2)
# Update Local State file
local_state_file = os.path.join(self.profile_dir, "Local State")
local_state_data = {}
if os.path.exists(local_state_file):
try:
with open(local_state_file, 'r') as f:
local_state_data = json.load(f)
except:
local_state_data = {}
# Ensure extensions section exists
if 'extensions' not in local_state_data:
local_state_data['extensions'] = {}
# Enable developer mode in local state
local_state_data['extensions']['ui'] = {
"developer_mode": True
}
# Write local state
with open(local_state_file, 'w') as f:
json.dump(local_state_data, f, indent=2)
print("Lovense extension reconfiguration complete during reload")
except Exception as e:
print(f"Error updating preferences file during reload: {e}")
self.profile.installUrlSchemeHandler(b"qextension", self.extension_scheme_handler)
# 5. Re-create all tabs with the new profile
if not urls: # Ensure there's at least one tab
urls.append(self.home_url)
for url in urls:
self.new_page(url)
# 6. Restore the previously active tab
if current_index != -1 and current_index < len(urls):
self.tabs.setCurrentIndex(current_index)
# 7. Reinitialize the content script injector
self.content_script_injector = ContentScriptInjector(self.extensions_dir)
# 8. Schedule the old profile for deletion. This is crucial.
old_profile.setParent(None)
old_profile.deleteLater()
print("Profile and tabs reloaded.")
self.update_extension_buttons()
def open_extension_popup_from_toolbar(self, ext_path):
"""Opens an extension's popup from a toolbar button click."""
ext_name = os.path.basename(ext_path)
manifest_path = os.path.join(ext_path, "manifest.json")
if not os.path.exists(manifest_path):
QMessageBox.warning(self, "Error", "manifest.json not found for this extension.")
return
try:
with open(manifest_path, 'r', encoding='utf-8') as f:
manifest = json.load(f)
except Exception as e:
QMessageBox.critical(self, "Error", f"Could not read manifest.json: {e}")
return
popup_path = None
action = manifest.get('action') or manifest.get('browser_action') or manifest.get('page_action')
if action and action.get('default_popup'):
popup_path = action.get('default_popup')
if not popup_path:
# This should not happen if the button was created, but as a safeguard:
QMessageBox.information(self, "No Popup", "This extension does not have a popup.")
return
popup_file = os.path.join(ext_path, popup_path)
if not os.path.exists(popup_file):
QMessageBox.warning(self, "Error", f"Popup file not found: {popup_path}")
return
# Open the popup in a new non-modal dialog
popup_dialog = QDialog(self)
popup_dialog.setWindowTitle(manifest.get("name", "Extension Popup"))
layout = QVBoxLayout(popup_dialog)
web_view = QWebEngineView()
# The 'self' here is the Browser instance, so self.profile is correct
page = QWebEnginePage(self.profile, web_view)
web_view.setPage(page)
# Set up QWebChannel for the page to enable chrome.runtime API
page.setWebChannel(self.web_channel)
# Explicitly initialize the QWebChannel with a script
init_script = """
// Create a global variable to track if transport is ready
window.__qtWebChannelTransportReady = false;
function initializeWebChannel() {
if (typeof qt !== 'undefined' && qt.webChannelTransport) {
try {
// Check if QWebChannel is defined
if (typeof QWebChannel === 'undefined') {
console.log("QWebChannel is not defined yet, will retry later");
return false;
}
new QWebChannel(qt.webChannelTransport, function(channel) {
window.channel = channel;
window.runtimeBridge = channel.objects.runtimeBridge;
window.__qtWebChannelTransportReady = true;
console.log("QWebChannel initialized successfully for extension popup");
// Dispatch an event to notify that the channel is ready
document.dispatchEvent(new CustomEvent('webChannelReady', {
detail: { channel: channel }
}));
// Also dispatch the chrome.runtime.initialized event
document.dispatchEvent(new CustomEvent('chrome.runtime.initialized', {
detail: { extensionId: "%s" }
}));
});
return true;
} catch (e) {
console.error("Error initializing QWebChannel:", e);
return false;
}
} else {
console.log("qt.webChannelTransport not available yet, will retry later");
return false;
}
}
// Try to initialize immediately
if (!initializeWebChannel()) {
// If it fails, set up a retry mechanism
let retryCount = 0;
const MAX_RETRIES = 50;
const retryInterval = setInterval(function() {
console.log("Retrying QWebChannel initialization... (attempt " + (retryCount + 1) + " of " + MAX_RETRIES + ")");
if (initializeWebChannel() || retryCount >= MAX_RETRIES) {
clearInterval(retryInterval);
if (retryCount >= MAX_RETRIES) {
console.error("Failed to initialize QWebChannel after " + MAX_RETRIES + " attempts");
}
}
retryCount++;
}, 200);
}
""" % ext_name
# Run the initialization script before loading the URL
page.runJavaScript(init_script, lambda result: print("QWebChannel initialization script executed for popup"))
popup_url = QUrl(f"qextension://{ext_name}/{popup_path}")
web_view.load(popup_url)
layout.addWidget(web_view)
popup_dialog.setLayout(layout)
popup_dialog.resize(400, 600)
popup_dialog.show()
# Keep a reference to prevent garbage collection
self.open_extension_popups.append(popup_dialog)
popup_dialog.finished.connect(lambda: self.open_extension_popups.remove(popup_dialog))
def update_extensions_list(self):
if not self.extensions_dir or not os.path.exists(self.extensions_dir):
return
# refresh the loaded_extension list
self.loaded_extensions = {}
# Find all enabled extension directories
for ext_name in sorted(os.listdir(self.extensions_dir)):
if ext_name.endswith(".disabled"):
continue
ext_path = os.path.join(self.extensions_dir, ext_name)
if not os.path.isdir(ext_path):
continue
manifest_path = os.path.join(ext_path, "manifest.json")
if not os.path.exists(manifest_path):
continue
try:
with open(manifest_path, 'r', encoding='utf-8') as f:
manifest = json.load(f)
except Exception as e:
print(f"Error loading manifest for {ext_name}: {e}")
continue
# Store the extension info
self.loaded_extensions[ext_name] = {
'path': ext_path,
'manifest': manifest,
'id': ext_name
}
def update_extension_buttons(self):
"""Scans for extensions and adds a button for each one with a popup to the main toolbar."""
# Remove previous extension buttons
for action in self.extension_actions:
self.toolbar.removeAction(action)
self.extension_actions.clear()
if not self.extensions_dir or not os.path.exists(self.extensions_dir):
return
self.update_extensions_list()
for ext_name in self.loaded_extensions.keys():
manifest = self.loaded_extensions[ext_name]['manifest']
ext_path = self.loaded_extensions[ext_name]['path']
# Check for a popup action
popup_path = None
action = manifest.get('action') or manifest.get('browser_action') or manifest.get('page_action')
if action and action.get('default_popup'):
popup_path = action.get('default_popup')
if popup_path:
# Create a button for this extension
button = QPushButton()
button.setFlat(True)
button.setIconSize(QSize(22, 22))
# Set the icon
icon_path = None
if manifest.get("icons"):
# Prefer a 24px or 16px icon for the toolbar
icon_path = manifest["icons"].get("24") or manifest["icons"].get("16") or next(iter(manifest["icons"].values()), None)
if icon_path:
full_icon_path = os.path.join(ext_path, icon_path)
if os.path.exists(full_icon_path):
button.setIcon(QIcon(full_icon_path))
button.setToolTip(manifest.get("name", ext_name))
button.clicked.connect(partial(self.open_extension_popup_from_toolbar, ext_path))
# Insert the button before the main "Extensions" button
action = self.toolbar.insertWidget(self.extensions_button_action, button)
self.extension_actions.append(action)
def launch_test_extension(self):
"""Launch the test extension in a new tab."""
# Check if the test extension is loaded
print(self.loaded_extensions)
if 'test-extension' not in self.loaded_extensions:
QMessageBox.warning(self, "Extension Not Found",
"The test extension was not found. Make sure it's in the assets/browser/extensions directory.")
return
ext_info = self.loaded_extensions['test-extension']
# First, launch the background page to ensure the background script is running
self.launch_extension_background('test-extension')
# Then open the popup in a new tab
manifest = ext_info['manifest']
popup_path = None
# Check for popup in the manifest
action = manifest.get('action') or manifest.get('browser_action') or manifest.get('page_action')
if action and action.get('default_popup'):
popup_path = action.get('default_popup')
if popup_path:
# Create a URL for the popup
popup_url = f"qextension://test-extension/{popup_path}"
# Open in a new tab
tab = self.new_page(popup_url)
self.tabs.setTabText(self.tabs.indexOf(tab), f"Test Extension: {manifest.get('name', 'Popup')}")
print(f"Launched test extension popup: {popup_url}")
else:
QMessageBox.warning(self, "No Popup", "The test extension does not have a popup defined in its manifest.")
def launch_extension_background(self, extension_id):
"""Launch an extension's background page or script in a hidden tab."""
print(self.loaded_extensions)
if extension_id not in self.loaded_extensions:
print(f"Extension {extension_id} not found")
return None
ext_info = self.loaded_extensions[extension_id]
manifest = ext_info['manifest']
# Check for background page or script
background = manifest.get('background', {})
background_page = background.get('page')
background_script = background.get('service_worker')
if background_page:
# Create a URL for the background page
bg_url = f"qextension://{extension_id}/{background_page}"
# Open in a new tab (could be hidden in a real implementation)
tab = self.new_page(bg_url)
self.tabs.setTabText(self.tabs.indexOf(tab), f"{extension_id} Background")
# Ensure WebChannel is set up for this tab
if hasattr(tab, 'web_view') and hasattr(tab.web_view, 'page'):
page = tab.web_view.page()
# First load QWebChannel.js content
qwebchannel_path = os.path.join("assets/browser/js", "qwebchannel.js")
qwebchannel_js = ""
try:
with open(qwebchannel_path, 'r', encoding='utf-8') as f:
qwebchannel_js = f.read()
except Exception as e:
print(f"Error loading QWebChannel.js: {e}")
qwebchannel_js = "console.error('Failed to load QWebChannel.js');"
# Explicitly initialize the QWebChannel with a script that includes QWebChannel.js
init_script = f"""
// Directly inject QWebChannel.js content
{qwebchannel_js}
console.log("QWebChannel.js directly injected into background page");
// Verify QWebChannel is defined
if (typeof QWebChannel === 'undefined') {{
console.error("QWebChannel is still undefined after direct injection!");
}} else {{
console.log("QWebChannel is successfully defined in global scope");
}}
// Create a global variable to track if transport is ready
window.__qtWebChannelTransportReady = false;
function initializeWebChannel() {{
if (typeof qt !== 'undefined' && qt.webChannelTransport) {{
try {{
// QWebChannel should be defined now
new QWebChannel(qt.webChannelTransport, function(channel) {{
window.channel = channel;
window.runtimeBridge = channel.objects.runtimeBridge;
window.__qtWebChannelTransportReady = true;
console.log("QWebChannel initialized successfully for background page");
// Dispatch an event to notify that the channel is ready
document.dispatchEvent(new CustomEvent('webChannelReady', {{
detail: {{ channel: channel }}
}}));
// Also dispatch the chrome.runtime.initialized event
document.dispatchEvent(new CustomEvent('chrome.runtime.initialized', {{
detail: {{ extensionId: "{extension_id}" }}
}}));
// Make chrome.runtime available globally
if (window.runtimeBridge) {{
console.log("RuntimeBridge is available, initializing chrome.runtime API");
// Expose chrome.runtime API
if (typeof chrome === 'undefined') {{
window.chrome = {{}};
}}
if (typeof chrome.runtime === 'undefined') {{
chrome.runtime = {{}};
}}
// Define basic chrome.runtime API methods
chrome.runtime.id = "{extension_id}";
chrome.runtime.sendMessage = function(extensionId, message, options, callback) {{
if (typeof extensionId !== 'string') {{
callback = options;
options = message;
message = extensionId;
extensionId = "{extension_id}";
}}
// Always pass an options parameter, even if it's empty
if (typeof options === 'undefined') {{
options = {{}};
}}
return window.runtimeBridge.sendMessage(extensionId, JSON.stringify(message), JSON.stringify(options));
}};
}}
}});
return true;
}} catch (e) {{
console.error("Error initializing QWebChannel:", e);
return false;
}}
}} else {{
console.log("qt.webChannelTransport not available yet, will retry later");
return false;
}}
}}
// Create a global variable to track initialization attempts
window.__qwebchannel_init_attempts = 0;
// Try to initialize immediately
if (!initializeWebChannel()) {{
// If it fails, set up a retry mechanism
const MAX_RETRIES = 50;
const retryInterval = setInterval(function() {{
window.__qwebchannel_init_attempts++;
console.log("Retrying QWebChannel initialization... (attempt " + window.__qwebchannel_init_attempts + " of " + MAX_RETRIES + ")");
// Check if qt object exists, if not, try to create it
if (typeof qt === 'undefined') {{
console.log("qt object is undefined, creating placeholder");
window.qt = {{}};
}}
if (initializeWebChannel() || window.__qwebchannel_init_attempts >= MAX_RETRIES) {{
clearInterval(retryInterval);
if (window.__qwebchannel_init_attempts >= MAX_RETRIES) {{
console.error("Failed to initialize QWebChannel after " + MAX_RETRIES + " attempts");
}}
}}
}}, 200);
}}
"""
# Run the initialization script
page.runJavaScript(init_script, lambda result: print("QWebChannel initialization script executed for background page"))
print(f"Launched extension background page: {bg_url}")
return tab
elif background_script:
# For service workers, we need to create a special page that loads the script
# This is a simplified approach - in a real implementation, you'd use a proper service worker container
# Create a temporary HTML file that loads the background script
temp_dir = os.path.join(self.profile_dir, "temp")
os.makedirs(temp_dir, exist_ok=True)
# Include WebChannel initialization in the HTML file
temp_html_path = os.path.join(temp_dir, f"{extension_id}_background.html")
with open(temp_html_path, 'w', encoding='utf-8') as f:
f.write(f"""<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{extension_id} Background</title>
<script>
// Create a global variable to track if transport is ready
window.__qtWebChannelTransportReady = false;
function initializeWebChannel() {{
if (typeof qt !== 'undefined' && qt.webChannelTransport) {{
try {{
// Check if QWebChannel is defined
if (typeof QWebChannel === 'undefined') {{
console.log("QWebChannel is not defined yet, will retry later");
return false;
}}
new QWebChannel(qt.webChannelTransport, function(channel) {{
window.channel = channel;
window.runtimeBridge = channel.objects.runtimeBridge;
window.__qtWebChannelTransportReady = true;
console.log("QWebChannel initialized successfully for background script");
// Dispatch an event to notify that the channel is ready
document.dispatchEvent(new CustomEvent('webChannelReady', {{
detail: {{ channel: channel }}
}}));
// Also dispatch the chrome.runtime.initialized event
document.dispatchEvent(new CustomEvent('chrome.runtime.initialized', {{
detail: {{ extensionId: "{extension_id}" }}
}}));
}});
return true;
}} catch (e) {{
console.error("Error initializing QWebChannel:", e);
return false;
}}
}} else {{
console.log("qt.webChannelTransport not available yet, will retry later");
return false;
}}
}}
// Try to initialize immediately
document.addEventListener('DOMContentLoaded', function() {{
if (!initializeWebChannel()) {{
// If it fails, set up a retry mechanism
let retryCount = 0;
const MAX_RETRIES = 50;
const retryInterval = setInterval(function() {{
console.log("Retrying QWebChannel initialization... (attempt " + (retryCount + 1) + " of " + MAX_RETRIES + ")");
if (initializeWebChannel() || retryCount >= MAX_RETRIES) {{
clearInterval(retryInterval);
if (retryCount >= MAX_RETRIES) {{
console.error("Failed to initialize QWebChannel after " + MAX_RETRIES + " attempts");
}}
}}
retryCount++;
}}, 200);
}}
}});
</script>
</head>
<body>
<h1>{extension_id} Background Script</h1>
<p>This page loads the background script for the extension.</p>
<script src="qextension://{extension_id}/{background_script}"></script>
</body>
</html>""")
# Open the temporary HTML file in a new tab
tab = self.new_page(f"file://{temp_html_path}")
self.tabs.setTabText(self.tabs.indexOf(tab), f"{extension_id} Background")
# Ensure WebChannel is set up for this tab
if hasattr(tab, 'web_view') and hasattr(tab.web_view, 'page'):
page = tab.web_view.page()
page.setWebChannel(self.web_channel)
print(f"Launched extension background script: {background_script}")
return tab
else:
print(f"Extension {extension_id} has no background page or script")
return None
def debug_chrome_urls(self):
"""Debug chrome:// URLs by showing information and testing navigation."""
debug_dialog = QDialog(self)
debug_dialog.setWindowTitle("Chrome URL Debug")
debug_dialog.resize(600, 500)
layout = QVBoxLayout(debug_dialog)
# Add information about the current configuration
info_text = QTextEdit()
info_text.setReadOnly(True)
# Collect debug information
debug_info = [
"Chrome URL Debug Information:",
"----------------------------",
f"Profile Path: {self.profile_dir}",
f"User Agent: {self.profile.httpUserAgent()}",
"WebEngine Settings:",
f" LocalContentCanAccessRemoteUrls: {self.profile.settings().testAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls)}",
f" ErrorPageEnabled: {self.profile.settings().testAttribute(QWebEngineSettings.WebAttribute.ErrorPageEnabled)}",
f" FocusOnNavigationEnabled: {self.profile.settings().testAttribute(QWebEngineSettings.WebAttribute.FocusOnNavigationEnabled)}",
f" AllowRunningInsecureContent: {self.profile.settings().testAttribute(QWebEngineSettings.WebAttribute.AllowRunningInsecureContent)}",
"Environment Variables:",
f" QTWEBENGINE_CHROMIUM_FLAGS: {os.environ.get('QTWEBENGINE_CHROMIUM_FLAGS', 'Not set')}",
"----------------------------",
"Instructions:",
"1. Click 'Test Chrome URL' to open chrome://version/ in a new tab",
"2. If it doesn't work, try restarting the browser with --debug flag",
"3. Check if the custom ChromeWebEnginePage is being used correctly"
]
info_text.setText("\n".join(debug_info))
layout.addWidget(info_text)
# Add buttons for testing
button_layout = QHBoxLayout()
test_button = QPushButton("Test Chrome URL")
test_button.clicked.connect(lambda: self.navigate_to_chrome_url("chrome://version/"))
button_layout.addWidget(test_button)
close_button = QPushButton("Close")
close_button.clicked.connect(debug_dialog.accept)
button_layout.addWidget(close_button)
layout.addLayout(button_layout)
debug_dialog.exec()
def show_extensions(self):
"""Show the extensions dialog."""
dialog = ExtensionDialog(self, self.extensions_dir)
dialog.exec()
class QPlaywrightBrowser:
"""
A class that wraps the Playwright browser API and uses Qt6Browser internally.
This class exposes the Playwright API while using the Qt6 browser for display.
"""
def __init__(self):
self.app = QApplication.instance() or QApplication(sys.argv)
self.browser_ui = None
self.playwright = None
self.browser = None
self.contexts = {}
self.event_loop = None
async def launch(self, **kwargs):
"""Launch a new browser instance."""
# Initialize Playwright if not already done
if not self.playwright:
self.playwright = await async_playwright().start()
# Create the Qt browser UI
if not self.browser_ui:
self.browser_ui = Browser()
self.browser_ui.show()
# Launch the actual Playwright browser (hidden)
# We'll use this for API compatibility but display in Qt
self.browser = await self.playwright.chromium.launch(
headless=True, # Run headless since we're displaying in Qt
args = [
'--headless=new',
'--enable-features=Autofill',
]+os.environ.get('QTWEBENGINE_CHROMIUM_FLAGS').split(),
**kwargs
)
#
# Create a default context
default_context = await self.browser.new_context()
self.contexts["default"] = default_context
return self
async def new_context(self, **kwargs):
"""Create a new browser context."""
if not self.browser:
await self.launch()
# Create a new context in the Playwright browser
context_id = f"context_{len(self.contexts)}"
context = await self.browser.new_context(**kwargs)
self.contexts[context_id] = context
# Return a wrapper that provides both Playwright API and Qt UI
return QPlaywrightBrowserContext(self, context, context_id)
async def new_page(self, url="about:blank"):
"""Create a new page in the default context."""
if "default" not in self.contexts:
await self.launch()
# Create a new page in the Playwright browser
pw_page = await self.contexts["default"].new_page()
await pw_page.goto(url)
# Create a new tab in the Qt browser
qt_tab = self.browser_ui.new_page(url)
qt_tab.page = pw_page
# Return a wrapper that provides both Playwright API and Qt UI
return QPlaywrightPage(pw_page, qt_tab)
async def close(self):
"""Close the browser."""
# Close all Playwright contexts and browser
if self.browser:
for context_id, context in self.contexts.items():
await context.close()
await self.browser.close()
self.contexts = {}
self.browser = None
# Close the Qt browser UI
if self.browser_ui:
self.browser_ui.close()
self.browser_ui = None
# Close Playwright
if self.playwright:
await self.playwright.stop()
self.playwright = None
def run(self):
"""Run the application event loop."""
return self.app.exec()
def run_async(self, coro):
"""Run an async coroutine in the Qt event loop."""
if not self.event_loop:
self.event_loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.event_loop)
# Create a QTimer to process asyncio events
timer = QTimer()
timer.timeout.connect(lambda: self.event_loop.run_until_complete(asyncio.sleep(0)))
timer.start(10) # 10ms interval
# Run the coroutine
future = asyncio.run_coroutine_threadsafe(coro, self.event_loop)
return future
class QPlaywrightBrowserContext:
"""
A wrapper around a Playwright BrowserContext that provides both
Playwright API and Qt UI integration.
"""
def __init__(self, qbrowser, context, context_id):
self.qbrowser = qbrowser
self.context = context
self.context_id = context_id
self.pages = []
async def new_page(self):
"""Create a new page in this context."""
# Create a new page in the Playwright context
pw_page = await self.context.new_page()
# Create a new tab in the Qt browser
qt_tab = self.qbrowser.browser_ui.new_page("about:blank")
qt_tab.page = pw_page
# Create a wrapper page
page = QPlaywrightPage(pw_page, qt_tab)
self.pages.append(page)
return page
async def close(self):
"""Close this context."""
# Close all pages
for page in self.pages:
await page.close()
# Close the Playwright context
await self.context.close()
# Remove from the qbrowser contexts
if self.context_id in self.qbrowser.contexts:
del self.qbrowser.contexts[self.context_id]
class QPlaywrightPage:
"""
A wrapper around a Playwright Page that provides both
Playwright API and Qt UI integration.
"""
def __init__(self, pw_page, qt_tab):
self.pw_page = pw_page
self.qt_tab = qt_tab
# Forward Playwright page methods and properties
self.goto = pw_page.goto
self.click = pw_page.click
self.fill = pw_page.fill
self.type = pw_page.type
self.press = pw_page.press
self.wait_for_selector = pw_page.wait_for_selector
self.wait_for_navigation = pw_page.wait_for_navigation
self.wait_for_load_state = pw_page.wait_for_load_state
self.evaluate = pw_page.evaluate
self.screenshot = pw_page.screenshot
self.content = pw_page.content
self.title = pw_page.title
self.url = pw_page.url
async def close(self):
"""Close this page."""
# Close the Playwright page
await self.pw_page.close()
# Close the Qt tab
# We need to use the Qt event loop for this
index = self.qt_tab.parent.indexOf(self.qt_tab)
if index >= 0:
self.qt_tab.parent.close_tab(index)
async def main_async():
"""Async main function to run the browser."""
browser = QPlaywrightBrowser()
await browser.launch()
# Open a page
url = "https://www.google.com"
if len(sys.argv) > 1:
url = sys.argv[1]
await browser.new_page(url)
# Keep the browser running
while True:
await asyncio.sleep(0.1)
def main():
"""Main function to run the browser as a standalone application."""
# Enable Autofill features and extension developer mode via environment variables
chromium_flags = "--enable-features=AutofillEnableAccountWalletStorage,AutofillAddressProfileSavePrompt,AutofillCreditCardUpload,AutofillEnableToolbarStatusChip,AutofillKeyboardAccessory,AutofillShowAllSuggestionsOnPrefsCheckout,AutofillShowTypePredictions,AutofillUpstream,PasswordGeneration,PasswordGenerationBottomSheetUI,PasswordGenerationExperiment,ExtensionsToolbarMenu,ChromeUIDebugTools --extensions-on-chrome-urls --allow-file-access-from-files --allow-running-insecure-content --enable-user-scripts --allow-universal-access-from-files --disable-web-security --disable-site-isolation-trials --allow-insecure-localhost --ignore-certificate-errors --ignore-urlfetcher-cert-requests --disable-features=BlockInsecurePrivateNetworkRequests"
# Always add extension developer mode flags
#chromium_flags += " --load-extension=assets/browser/extensions --force-dev-mode-highlighting"
chromium_flags += " --force-dev-mode-highlighting"
# Explicitly enable chrome:// URLs
chromium_flags += " --enable-chrome-urls --enable-features=ChromeUIDebugTools"
# Chrome scheme is already registered in register_url_schemes()
os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = chromium_flags
debug_mode = "--debug" in sys.argv
#Print environment variables for debugging and set remote debug where needed
print(f"QTWEBENGINE_CHROMIUM_FLAGS: {os.environ.get('QTWEBENGINE_CHROMIUM_FLAGS')}")
if debug_mode:
os.environ["QTWEBENGINE_REMOTE_DEBUGGING"] = "9222"
print(f"QTWEBENGINE_REMOTE_DEBUGGING: {os.environ.get('QTWEBENGINE_REMOTE_DEBUGGING')}")
app = QApplication.instance() or QApplication(sys.argv)
# Allow Ctrl+C to kill the application gracefully
signal.signal(signal.SIGINT, signal.SIG_DFL)
# Set application logo
app.setWindowIcon(QIcon('assets/logo.jpg'))
# Set a modern, dark theme
app.setStyle("Fusion")
dark_palette = app.palette()
dark_palette.setColor(dark_palette.ColorRole.Window, QColor(45, 45, 45))
dark_palette.setColor(dark_palette.ColorRole.WindowText, Qt.GlobalColor.white)
dark_palette.setColor(dark_palette.ColorRole.Base, QColor(25, 25, 25))
dark_palette.setColor(dark_palette.ColorRole.AlternateBase, QColor(53, 53, 53))
dark_palette.setColor(dark_palette.ColorRole.ToolTipBase, QColor(25, 25, 25))
dark_palette.setColor(dark_palette.ColorRole.ToolTipText, Qt.GlobalColor.white)
dark_palette.setColor(dark_palette.ColorRole.Text, Qt.GlobalColor.white)
dark_palette.setColor(dark_palette.ColorRole.Button, QColor(53, 53, 53))
dark_palette.setColor(dark_palette.ColorRole.ButtonText, Qt.GlobalColor.white)
dark_palette.setColor(dark_palette.ColorRole.BrightText, Qt.GlobalColor.red)
dark_palette.setColor(dark_palette.ColorRole.Link, QColor(42, 130, 218))
dark_palette.setColor(dark_palette.ColorRole.Highlight, QColor(42, 130, 218))
dark_palette.setColor(dark_palette.ColorRole.HighlightedText, Qt.GlobalColor.black)
app.setPalette(dark_palette)
# Set a better font
font = QFont("Cantarell", 10)
if platform.system() == "Windows":
font = QFont("Segoe UI", 10)
elif platform.system() == "Darwin":
font = QFont("San Francisco", 10)
app.setFont(font)
# Create a dummy parent for dialogs to ensure they are properly modal
dummy_parent = QWidget()
# Get profile path
profile_path = ProfileDialog.get_profile_path(dummy_parent)
if not profile_path:
return 0 # Exit gracefully
# Create a simple browser directly without Playwright for now
# This ensures we at least get a window showing
initial_url = None
if len(sys.argv) > 1 and not sys.argv[1].startswith('--'):
initial_url = sys.argv[1]
browser = Browser(initial_url=initial_url, debug=debug_mode, profile_path=profile_path)
browser.show()
print("Browser window should be visible now")
# Run the Qt event loop
return app.exec()
# Register schemes before anything else
register_url_schemes()
if __name__ == "__main__":
sys.exit(main())
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment