You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

374 lines
12 KiB

# -*- coding: utf-8 -*-
#
# browser.py
#
# Copyright © 2017 Antergos
# Copyright © 2021 JezerM
#
# This file is part of Web Greeter.
#
# Web Greeter 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.
#
# Web Greeter 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.
#
# The following additional terms are in effect as per Section 7 of the license:
#
# The preservation of all legal notices and author attributions in
# the material or in the Appropriate Legal Notices displayed
# by works containing it is required.
#
# You should have received a copy of the GNU General Public License
# along with Web Greeter; If not, see <http://www.gnu.org/licenses/>.
# Standard lib
import re
from browser.window import MainWindow
import os
from typing import (
Dict,
Tuple,
TypeVar,
)
# 3rd-Party Libs
from PyQt5.QtCore import QRect, QUrl, Qt, QCoreApplication, QFile, QSize
from PyQt5.QtWidgets import QAction, QApplication, QDesktopWidget, QDockWidget, QMainWindow, QLayout, qApp, QWidget
from PyQt5.QtWebEngineCore import QWebEngineUrlScheme
from PyQt5.QtWebEngineWidgets import QWebEngineScript, QWebEngineProfile, QWebEngineSettings, QWebEngineView, QWebEnginePage
from PyQt5.QtGui import QColor, QIcon
from PyQt5.QtWebChannel import QWebChannel
from browser.error_prompt import WebPage
from browser.url_scheme import QtUrlSchemeHandler
from browser.interceptor import QtUrlRequestInterceptor
from logger import logger
from config import web_greeter_config
from bridge import Greeter, Config, ThemeUtils
from utils.screensaver import reset_screensaver, set_screensaver, init_display
import resources
# Typing Helpers
BridgeObjects = Tuple['BridgeObject']
Url = TypeVar('Url', str, QUrl)
os.environ["QT_DEVICE_PIXEL_RATIO"] = "0"
os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"
os.environ["QT_SCREEN_SCALE_FACTORS"] = "1"
os.environ["QT_SCALE_FACTOR"] = "1"
WINDOW_STATES = {
'NORMAL': Qt.WindowState.WindowNoState,
'MINIMIZED': Qt.WindowState.WindowMinimized,
'MAXIMIZED': Qt.WindowState.WindowMaximized,
'FULLSCREEN': Qt.WindowState.WindowFullScreen,
} # type: Dict[str, Qt.WindowState]
DISABLED_SETTINGS = [
'PluginsEnabled', # Qt 5.6+
]
ENABLED_SETTINGS = [
'FocusOnNavigationEnabled', # Qt 5.8+
'FullScreenSupportEnabled', # Qt 5.6+
'LocalContentCanAccessFileUrls',
'ScreenCaptureEnabled', # Qt 5.7+
'ScrollAnimatorEnabled',
'FocusOnNavigationEnabled', # Qt 5.11+
]
def getDefaultCursor():
cursor_theme = ""
file = open("/usr/share/icons/default/index.theme")
matched = re.search(r"Inherits=.*", file.read())
file.close()
if not matched:
logger.error("Default cursor couldn't be get")
return ""
cursor_theme = matched.group().replace("Inherits=", "")
return cursor_theme
class Application:
app: QApplication
desktop: QDesktopWidget
window: QMainWindow
states = WINDOW_STATES
def __init__(self):
QCoreApplication.setAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling)
QApplication.setAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling)
self.app = QApplication([])
self.window = MainWindow()
self.desktop = self.app.desktop()
self.window.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
self.window.setWindowTitle("Web Greeter")
self.window.setWindowFlags(
self.window.windowFlags() | Qt.WindowType.MaximizeUsingFullscreenGeometryHint
)
if web_greeter_config["app"]["frame"]:
self._init_menu_bar()
else:
self.window.setWindowFlags(self.window.windowFlags() | Qt.WindowType.FramelessWindowHint)
screen_size = self.desktop.availableGeometry().size()
self.window.setBaseSize(screen_size)
self.window.resize(screen_size)
state = self.states['NORMAL']
if web_greeter_config["app"]["fullscreen"]:
state = self.states["FULLSCREEN"]
try:
self.window.windowHandle().setWindowState(state)
except Exception:
self.window.setWindowState(state)
self.window.setCursor(Qt.CursorShape.ArrowCursor)
init_display()
timeout = web_greeter_config["config"]["greeter"]["screensaver_timeout"]
set_screensaver(timeout or 300)
cursor_theme = web_greeter_config["config"]["greeter"]["icon_theme"]
os.environ["XCURSOR_THEME"] = cursor_theme if cursor_theme != None else getDefaultCursor()
self.app.aboutToQuit.connect(self._before_exit)
def _before_exit(self):
reset_screensaver()
def show(self):
self.window.show()
logger.debug("Window is ready")
def run(self) -> int:
logger.debug("Web Greeter started")
return self.app.exec_()
def _init_menu_bar(self):
exit_action = QAction(QIcon('exit.png'), '&Exit', self.window)
exit_action.setShortcut('Ctrl+Q')
exit_action.setStatusTip('Exit application')
exit_action.triggered.connect(qApp.quit)
menu_bar = self.window.menuBar()
file_menu = menu_bar.addMenu('&File')
file_menu.addAction(exit_action)
edit_menu = menu_bar.addMenu('&Edit')
edit_menu.addAction(exit_action)
view_menu = menu_bar.addMenu('&View')
view_menu.addAction(exit_action)
about_menu = menu_bar.addMenu('&About')
about_menu.addAction(exit_action)
class NoneLayout(QLayout):
def __init__(self):
super().__init__()
def sizeHint(self) -> QSize:
size = QSize(0, 0)
return size
def minimumSizeHint(self) -> QSize:
size = QSize(0, 0)
return size
class Browser(Application):
url_scheme: QWebEngineUrlScheme
def __init__(self):
super().__init__()
self.init()
self.load()
def init(self):
logger.debug("Initializing Browser Window")
if web_greeter_config["config"]["greeter"]["debug_mode"]:
os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = '12345'
url_scheme = "web-greeter"
self.url_scheme = QWebEngineUrlScheme(url_scheme.encode())
self.url_scheme.setDefaultPort(QWebEngineUrlScheme.SpecialPort.PortUnspecified)
self.url_scheme.setFlags(QWebEngineUrlScheme.Flag.SecureScheme or
QWebEngineUrlScheme.Flag.LocalScheme or
QWebEngineUrlScheme.Flag.LocalAccessAllowed)
QWebEngineUrlScheme.registerScheme(self.url_scheme)
self.profile = QWebEngineProfile.defaultProfile()
self.interceptor = QtUrlRequestInterceptor(url_scheme)
self.url_scheme_handler = QtUrlSchemeHandler()
self.view = QWebEngineView(parent=self.window)
self.page = WebPage()
self.view.setPage(self.page)
self.channel = QWebChannel(self.page)
self.bridge_initialized = False
self.profile.installUrlSchemeHandler(url_scheme.encode(), self.url_scheme_handler)
self._initialize_page()
if web_greeter_config["config"]["greeter"]["debug_mode"]:
self._initialize_devtools()
if web_greeter_config["config"]["greeter"]["secure_mode"]:
self.profile.setUrlRequestInterceptor(self.interceptor)
self.page.setBackgroundColor(QColor(0, 0, 0))
self.window.setStyleSheet("""QMainWindow, QWebEngineView {
background: #000000;
}""")
self.window.setCentralWidget(self.view)
logger.debug("Browser Window created")
self.show()
def load(self):
self.greeter = Greeter()
self.greeter_config = Config()
self.theme_utils = ThemeUtils(self.greeter)
self.bridge_objects = (self.greeter, self.greeter_config, self.theme_utils)
self.initialize_bridge_objects()
self.load_script(':/_greeter/js/bundle.js', 'Web Greeter Bundle')
self.load_theme()
def _initialize_devtools(self):
self.dev_view = QWebEngineView(parent=self.window)
self.dev_page = QWebEnginePage()
self.dev_view.setPage(self.dev_page)
self.page.setDevToolsPage(self.dev_page)
self.qdock = QDockWidget()
self.qdock.setWidget(self.dev_view)
titlebar = QWidget(self.qdock)
layout = NoneLayout()
titlebar.setLayout(layout)
self.qdock.setTitleBarWidget(titlebar)
self.window.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.qdock)
self.qdock.hide()
logger.debug("DevTools initialized")
def toggle_devtools(self):
if not web_greeter_config["config"]["greeter"]["debug_mode"]:
return
win_size = self.window.size()
# dev_size = self.qdock.size()
self.qdock.resize(int(win_size.width() / 2), int(win_size.height()))
if self.qdock.isVisible():
self.qdock.hide()
self.view.setFocus()
else:
self.qdock.show()
self.dev_view.setFocus()
def _initialize_page(self):
page_settings = self.page.settings().globalSettings()
if not web_greeter_config["config"]["greeter"]["secure_mode"]:
ENABLED_SETTINGS.append('LocalContentCanAccessRemoteUrls')
else:
DISABLED_SETTINGS.append('LocalContentCanAccessRemoteUrls')
for setting in DISABLED_SETTINGS:
try:
page_settings.setAttribute(getattr(QWebEngineSettings, setting), False)
except AttributeError:
pass
for setting in ENABLED_SETTINGS:
try:
page_settings.setAttribute(getattr(QWebEngineSettings, setting), True)
except AttributeError:
pass
self.page.setView(self.view)
def load_theme(self):
theme = web_greeter_config["config"]["greeter"]["theme"]
dir = "/usr/share/web-greeter/themes/"
path_to_theme = os.path.join(dir, theme, "index.html")
def_theme = "gruvbox"
if (theme.startswith("/")): path_to_theme = theme
elif (theme.__contains__(".") or theme.__contains__("/")):
path_to_theme = os.path.join(os.getcwd(), theme)
if (not path_to_theme.endswith(".html")):
path_to_theme = os.path.join(path_to_theme, "index.html")
if (not os.path.exists(path_to_theme)):
print("Path does not exists")
path_to_theme = os.path.join(dir, def_theme, "index.html")
url = QUrl("web-greeter://app/{0}".format(path_to_theme))
self.page.load(url)
logger.debug("Theme loaded")
@staticmethod
def _create_webengine_script(path: Url, name: str) -> QWebEngineScript:
script = QWebEngineScript()
script_file = QFile(path)
# print(script_file, path)
if script_file.open(QFile.OpenModeFlag.ReadOnly):
script_string = str(script_file.readAll(), 'utf-8')
script.setInjectionPoint(QWebEngineScript.DocumentCreation)
script.setName(name)
script.setWorldId(QWebEngineScript.MainWorld)
script.setSourceCode(script_string)
# print(script_string)
return script
def _get_channel_api_script(self) -> QWebEngineScript:
return self._create_webengine_script(':/qtwebchannel/qwebchannel.js', 'QWebChannel API')
def _init_bridge_channel(self) -> None:
self.page.setWebChannel(self.channel)
self.page.scripts().insert(self._get_channel_api_script())
self.bridge_initialized = True
def initialize_bridge_objects(self) -> None:
if not self.bridge_initialized:
self._init_bridge_channel()
registered_objects = self.channel.registeredObjects()
for obj in self.bridge_objects:
if obj not in registered_objects:
self.channel.registerObject(obj._name, obj)
# print("Registered", obj._name)
def load_script(self, path: Url, name: str):
script = self._create_webengine_script(path, name)
self.page.scripts().insert(script)