12 Commits
v0.1.5 ... main

Author SHA1 Message Date
59c38f9c57 chore(changelog): update
All checks were successful
Code check / Check code (push) Successful in 1m15s
renovate / renovate (push) Successful in 28s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-09-06 12:44:43 +05:00
a2d5d28884 fix(cache): add cleanup of related cache files on JSON updates
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-09-06 12:43:22 +05:00
16af4b410a chore(renovate): disable almost python-version update
All checks were successful
Code check / Check code (push) Successful in 1m7s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-09-03 19:17:13 +05:00
e8e42b5a86 chore(renovate): disable python-version update
All checks were successful
Code check / Check code (push) Successful in 1m7s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-09-03 19:08:58 +05:00
d16e2cdf43 chore(renovate): dont update github-runners
All checks were successful
Code check / Check code (push) Successful in 1m44s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-09-02 22:56:03 +05:00
Renovate Bot
b60fd0d593 chore(deps): pin dependencies
All checks were successful
Code check / Check code (pull_request) Successful in 2m16s
Code check / Check code (push) Successful in 1m36s
2025-09-02 17:31:21 +00:00
d93f23fe8c chore(renovate): added GITHUB_TOKEN
All checks were successful
Code check / Check code (push) Successful in 1m15s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-09-02 22:28:10 +05:00
5423ada8f1 fix(theme-security): check standart theme too
All checks were successful
Code check / Check code (push) Successful in 1m12s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-09-02 17:05:35 +05:00
2547c7c78d chore(changelog): update
All checks were successful
Code check / Check code (push) Successful in 1m9s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-09-02 00:11:35 +05:00
2e93073446 feat(theme-security): add theme safety checks and unify loading via ThemeManager
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-09-01 23:58:38 +05:00
Gitea Actions
9657ff20d3 chore: update steam apps list 2025-09-01T15:10:40Z 2025-09-01 15:10:40 +00:00
849333c283 feat(dev-scripts): add import and function safety checks to theme pre-commit
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-09-01 11:42:06 +05:00
23 changed files with 12798 additions and 1017 deletions

View File

@@ -12,7 +12,7 @@ jobs:
name: Build AppImage
runs-on: ubuntu-22.04
steps:
- uses: https://gitea.com/actions/checkout@v4
- uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- name: Install required dependencies
run: |
@@ -63,7 +63,7 @@ jobs:
echo '%_topdir /home/rpmbuild' > /home/rpmbuild/.rpmmacros
- name: Checkout repo
uses: https://gitea.com/actions/checkout@v4
uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- name: Copy fedora.spec
run: |
@@ -84,7 +84,7 @@ jobs:
name: Build Arch Package
runs-on: ubuntu-22.04
container:
image: archlinux:base-devel
image: archlinux:base-devel@sha256:8ccc930c28ab4f483ff9bc1b53957150fbe94afe48928ebb0b14a8af41c75023
volumes:
- /usr:/usr-host
- /opt:/opt-host
@@ -124,7 +124,7 @@ jobs:
su user -c "yes '' | makepkg --noconfirm -s -p PKGBUILD-git"
- name: Checkout
uses: https://gitea.com/actions/checkout@v4
uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- name: Upload Arch package
uses: https://gitea.com/actions/gitea-upload-artifact@v4

View File

@@ -15,10 +15,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: https://gitea.com/actions/checkout@v4
uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- name: Set up Python
uses: https://gitea.com/actions/setup-python@v5
uses: https://gitea.com/actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version-file: "pyproject.toml"

View File

@@ -18,7 +18,7 @@ jobs:
fedora: ${{ steps.check.outputs.fedora }}
arch: ${{ steps.check.outputs.arch }}
steps:
- uses: https://gitea.com/actions/checkout@v4
- uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
with:
fetch-depth: 0
@@ -63,7 +63,7 @@ jobs:
needs: changes
if: needs.changes.outputs.appimage == 'true' || github.event_name == 'workflow_dispatch'
steps:
- uses: https://gitea.com/actions/checkout@v4
- uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- name: Install required dependencies
run: |
@@ -115,7 +115,7 @@ jobs:
echo '%_topdir /home/rpmbuild' > /home/rpmbuild/.rpmmacros
- name: Checkout repo
uses: https://gitea.com/actions/checkout@v4
uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- name: Copy fedora-git.spec
run: |
@@ -138,7 +138,7 @@ jobs:
needs: changes
if: needs.changes.outputs.arch == 'true' || github.event_name == 'workflow_dispatch'
container:
image: archlinux:base-devel
image: archlinux:base-devel@sha256:8ccc930c28ab4f483ff9bc1b53957150fbe94afe48928ebb0b14a8af41c75023
volumes:
- /usr:/usr-host
- /opt:/opt-host
@@ -178,7 +178,7 @@ jobs:
su user -c "yes '' | makepkg --noconfirm -s -p PKGBUILD-git"
- name: Checkout
uses: https://gitea.com/actions/checkout@v4
uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- name: Upload Arch package
uses: https://gitea.com/actions/gitea-upload-artifact@v4

View File

@@ -20,10 +20,10 @@ jobs:
name: Check code
runs-on: ubuntu-latest
steps:
- uses: https://gitea.com/actions/checkout@v4
- uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- name: Set up Node.js
uses: https://gitea.com/actions/setup-node@v4
uses: https://gitea.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 20

View File

@@ -11,10 +11,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: https://gitea.com/actions/checkout@v4
uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- name: Set up Python
uses: https://gitea.com/actions/setup-python@v5
uses: https://gitea.com/actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version-file: "pyproject.toml"

View File

@@ -8,12 +8,12 @@ on:
jobs:
renovate:
runs-on: ubuntu-latest
container: ghcr.io/renovatebot/renovate:latest
container: ghcr.io/renovatebot/renovate:latest@sha256:46b57bb9816dec6409e7be57e0e5f7b26d214281044f5aedd3b160be178475e2
steps:
- uses: https://gitea.com/actions/checkout@v4
- uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- name: Set up Node.js
uses: https://gitea.com/actions/setup-node@v4
uses: https://gitea.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 20
@@ -35,3 +35,4 @@ jobs:
RENOVATE_CONFIG_FILE: "/tmp/renovate-config/config.js"
LOG_LEVEL: "debug"
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
RENOVATE_GITHUB_COM_TOKEN: ${{ secrets.RENOVATE_GITHUB_COM_TOKEN }}

View File

@@ -3,6 +3,20 @@
Все заметные изменения в этом проекте фиксируются в этом файле.
Формат основан на [Keep a Changelog](https://keepachangelog.com/) и придерживается принципов [Semantic Versioning](https://semver.org/).
## [Unreleased]
### Added
### Changed
- Добавлена проверка безопасности в theme_manager.py для всех сторонних тем, с проверкой на запрещённые модули и функции (подробности см. в коде theme_manager под полями FORBIDDEN_MODULES и FORBIDDEN_FUNCTIONS)
### Fixed
- Исправлена проблема с устаревшими кэш-файлами, вызывающими несоответствия при обновлении JSON
### Contributors
---
## [0.1.5] - 2025-08-31
### Added

View File

@@ -1777,7 +1777,7 @@
},
{
"normalized_name": "supervive",
"status": "Denied"
"status": "Running"
},
{
"normalized_name": "splitgate 2",
@@ -4472,7 +4472,7 @@
"status": "Running"
},
{
"normalized_name": "f1 25",
"normalized_name": "battlefield 6",
"status": "Denied"
},
{
@@ -4482,5 +4482,65 @@
{
"normalized_name": "sword of justice",
"status": "Broken"
},
{
"normalized_name": "blade & soul neo",
"status": "Broken"
},
{
"normalized_name": "the finals (cn)",
"status": "Broken"
},
{
"normalized_name": "tom clancy's rainbow six siege x",
"status": "Denied"
},
{
"normalized_name": "dragonheir silent gods",
"status": "Broken"
},
{
"normalized_name": "the quinfall",
"status": "Running"
},
{
"normalized_name": "redmatch 2",
"status": "Broken"
},
{
"normalized_name": "blade & soul heroes",
"status": "Broken"
},
{
"normalized_name": "blue archive",
"status": "Running"
},
{
"normalized_name": "midnight murder club",
"status": "Broken"
},
{
"normalized_name": "dungeon done",
"status": "Broken"
},
{
"normalized_name": "project wraith",
"status": "Broken"
},
{
"normalized_name": "solo leveling arise",
"status": "Broken"
},
{
"normalized_name": "freedom wars",
"status": "Running"
},
{
"normalized_name": "open fortress",
"status": "Running"
},
{
"normalized_name": "no more room in hell 2",
"status": "Running"
}
]

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -1,4 +1,48 @@
[
{
"normalized_title": "astroneer",
"slug": "astroneer"
},
{
"normalized_title": "anno 2205",
"slug": "anno-2205"
},
{
"normalized_title": "anno 2070",
"slug": "anno-2070"
},
{
"normalized_title": "kompas 3d v23 / компас 3d v23",
"slug": "kompas-3d-v23-kompas-3d-v23"
},
{
"normalized_title": "ultrakill (early access)",
"slug": "ultrakill-early-access"
},
{
"normalized_title": "vintage story",
"slug": "vintage-story"
},
{
"normalized_title": "disco elysium the finul cut",
"slug": "disco-elysium-the-finul-cut"
},
{
"normalized_title": "warcraft iii reign of chaos",
"slug": "warcraft-iii-reign-of-chaos"
},
{
"normalized_title": "dying light",
"slug": "dying-light"
},
{
"normalized_title": "лихо одноглазое",
"slug": "liho-odnoglazoe"
},
{
"normalized_title": "indika",
"slug": "indika"
},
{
"normalized_title": "no sleep for kaname date from ai the somnium files",
"slug": "no-sleep-for-kaname-date-from-ai-the-somnium-files"
@@ -235,10 +279,6 @@
"normalized_title": "cardlife creative survival",
"slug": "cardlife-creative-survival"
},
{
"normalized_title": "kompas 3d v23 / компас 3d v23",
"slug": "kompas-3d-v23-kompas-3d-v23"
},
{
"normalized_title": "kompas 3d v24 / компас 3d v24 beta",
"slug": "kompas-3d-v24-kompas-3d-v24-beta"

Binary file not shown.

View File

@@ -3,8 +3,9 @@
import sys
from pathlib import Path
import re
import ast
# Запрещенные свойства
# Запрещенные QSS-свойства
FORBIDDEN_PROPERTIES = {
"box-shadow",
"backdrop-filter",
@@ -12,15 +13,55 @@ FORBIDDEN_PROPERTIES = {
"text-shadow",
}
# Запрещенные модули и функции
FORBIDDEN_MODULES = {
"os",
"subprocess",
"shutil",
"sys",
"socket",
"ctypes",
"pathlib",
"glob",
}
FORBIDDEN_FUNCTIONS = {
"exec",
"eval",
"open",
"__import__",
}
def check_qss_files():
has_errors = False
for qss_file in Path("portprotonqt/themes").glob("**/*.py"):
with open(qss_file, "r") as f:
content = f.read()
# Проверка на запрещённые QSS-свойства
for prop in FORBIDDEN_PROPERTIES:
if re.search(rf"{prop}\s*:", content, re.IGNORECASE):
print(f"ERROR: Unknown qss property found '{prop}' on file {qss_file}")
print(f"ERROR: Unknown QSS property found '{prop}' in file {qss_file}")
has_errors = True
# Проверка на опасные импорты и функции
try:
tree = ast.parse(content)
for node in ast.walk(tree):
# Проверка импортов
if isinstance(node, (ast.Import, ast.ImportFrom)):
for name in node.names:
if name.name in FORBIDDEN_MODULES:
print(f"ERROR: Forbidden module '{name.name}' found in file {qss_file}")
has_errors = True
# Проверка вызовов функций
if isinstance(node, ast.Call):
if isinstance(node.func, ast.Name) and node.func.id in FORBIDDEN_FUNCTIONS:
print(f"ERROR: Forbidden function '{node.func.id}' found in file {qss_file}")
has_errors = True
except SyntaxError as e:
print(f"ERROR: Syntax error in file {qss_file}: {e}")
has_errors = True
return has_errors
if __name__ == "__main__":

View File

@@ -2,8 +2,10 @@ from PySide6.QtCore import QPropertyAnimation, QByteArray, QEasingCurve, QAbstra
from PySide6.QtGui import QPainter, QPen, QColor, QConicalGradient, QBrush
from PySide6.QtWidgets import QWidget, QGraphicsOpacityEffect
from collections.abc import Callable
import portprotonqt.themes.standart.styles as default_styles
from portprotonqt.logger import get_logger
from portprotonqt.config_utils import read_theme_from_config
from portprotonqt.theme_manager import ThemeManager
logger = get_logger(__name__)
@@ -23,7 +25,8 @@ class SafeOpacityEffect(QGraphicsOpacityEffect):
class GameCardAnimations:
def __init__(self, game_card, theme=None):
self.game_card = game_card
self.theme = theme if theme is not None else default_styles
self.theme_manager = ThemeManager()
self.theme = theme if theme is not None else self.theme_manager.apply_theme(read_theme_from_config())
self.thickness_anim: QPropertyAnimation | None = None
self.gradient_anim: QPropertyAnimation | None = None
self.scale_anim: QPropertyAnimation | None = None
@@ -232,7 +235,8 @@ class GameCardAnimations:
class DetailPageAnimations:
def __init__(self, main_window, theme=None):
self.main_window = main_window
self.theme = theme if theme is not None else default_styles
self.theme_manager = ThemeManager()
self.theme = theme if theme is not None else self.theme_manager.apply_theme(read_theme_from_config())
self.animations = main_window._animations if hasattr(main_window, '_animations') else {}
def animate_detail_page(self, detail_page: QWidget, load_image_and_restore_effect: Callable, cleanup_animation: Callable):

View File

@@ -9,10 +9,9 @@ from PySide6.QtWidgets import (
from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer
from icoextract import IconExtractor, IconExtractorError
from PIL import Image
from portprotonqt.config_utils import get_portproton_location, read_favorite_folders
from portprotonqt.config_utils import get_portproton_location, read_favorite_folders, read_theme_from_config
from portprotonqt.localization import _
from portprotonqt.logger import get_logger
import portprotonqt.themes.standart.styles as default_styles
from portprotonqt.theme_manager import ThemeManager
from portprotonqt.custom_widgets import AutoSizeButton
from portprotonqt.downloader import Downloader
@@ -94,8 +93,8 @@ class GameLaunchDialog(QDialog):
"""Modal dialog to indicate game launch progress, similar to Steam's launch dialog."""
def __init__(self, parent=None, game_name=None, theme=None, target_exe=None):
super().__init__(parent)
self.theme = theme if theme else default_styles
self.theme_manager = ThemeManager()
self.theme = theme if theme else self.theme_manager.apply_theme(read_theme_from_config())
self.game_name = game_name
self.target_exe = target_exe # Store the target executable name
self.setWindowTitle(_("Launching {0}").format(self.game_name))
@@ -173,8 +172,8 @@ class GameLaunchDialog(QDialog):
class FileExplorer(QDialog):
def __init__(self, parent=None, theme=None, file_filter=None, initial_path=None, directory_only=False):
super().__init__(parent)
self.theme = theme if theme else default_styles
self.theme_manager = ThemeManager()
self.theme = theme if theme else self.theme_manager.apply_theme(read_theme_from_config())
self.file_signal = FileSelectedSignal()
self.file_filter = file_filter # Store the file filter
self.directory_only = directory_only # Store the directory_only flag
@@ -590,8 +589,8 @@ class AddGameDialog(QDialog):
def __init__(self, parent=None, theme=None, edit_mode=False, game_name=None, exe_path=None, cover_path=None):
super().__init__(parent)
from portprotonqt.context_menu_manager import CustomLineEdit # Локальный импорт
self.theme = theme if theme else default_styles
self.theme_manager = ThemeManager()
self.theme = theme if theme else self.theme_manager.apply_theme(read_theme_from_config())
self.edit_mode = edit_mode
self.original_name = game_name
self.last_exe_path = exe_path # Store last selected exe path

View File

@@ -2,12 +2,10 @@ from PySide6.QtGui import QPainter, QColor, QDesktopServices
from PySide6.QtCore import Signal, Property, Qt, QUrl
from PySide6.QtWidgets import QFrame, QGraphicsDropShadowEffect, QVBoxLayout, QWidget, QStackedLayout, QLabel
from collections.abc import Callable
import portprotonqt.themes.standart.styles as default_styles
from portprotonqt.image_utils import load_pixmap_async, round_corners
from portprotonqt.localization import _
from portprotonqt.config_utils import read_favorites, save_favorites, read_display_filter
from portprotonqt.config_utils import read_favorites, save_favorites, read_display_filter, read_theme_from_config
from portprotonqt.theme_manager import ThemeManager
from portprotonqt.config_utils import read_theme_from_config
from portprotonqt.custom_widgets import ClickableLabel
from portprotonqt.portproton_api import PortProtonAPI
from portprotonqt.downloader import Downloader
@@ -56,7 +54,7 @@ class GameCard(QFrame):
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.customContextMenuRequested.connect(self._show_context_menu)
self.theme_manager = ThemeManager()
self.theme = theme if theme is not None else default_styles
self.theme = theme if theme is not None else self.theme_manager.apply_theme(read_theme_from_config())
self.display_filter = read_display_filter()
self.current_theme_name = read_theme_from_config()

View File

@@ -3,7 +3,6 @@ from PySide6.QtGui import QPen, QColor, QPixmap, QPainter, QPainterPath
from PySide6.QtCore import Qt, QFile, QEvent, QByteArray, QEasingCurve, QPropertyAnimation
from PySide6.QtWidgets import QGraphicsItem, QToolButton, QFrame, QLabel, QGraphicsScene, QHBoxLayout, QWidget, QGraphicsView, QVBoxLayout, QSizePolicy
from PySide6.QtWidgets import QSpacerItem, QGraphicsPixmapItem, QDialog, QApplication
import portprotonqt.themes.standart.styles as default_styles
from portprotonqt.config_utils import read_theme_from_config
from portprotonqt.theme_manager import ThemeManager
from portprotonqt.downloader import Downloader
@@ -177,7 +176,8 @@ class FullscreenDialog(QDialog):
self.images = images
self.current_index = current_index
self.theme = theme if theme else default_styles
self.theme_manager = ThemeManager()
self.theme = theme if theme is not None else self.theme_manager.apply_theme(read_theme_from_config())
self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Dialog)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
@@ -378,7 +378,8 @@ class ImageCarousel(QGraphicsView):
self.images = images # Список кортежей: (QPixmap, caption)
self.image_items = []
self._animation = None
self.theme = theme if theme else default_styles
self.theme_manager = ThemeManager()
self.theme = theme if theme is not None else self.theme_manager.apply_theme(read_theme_from_config())
self.max_height = 300 # Default height for images
self.init_ui()
self.create_arrows()

View File

@@ -4,10 +4,9 @@ import shutil
import signal
import subprocess
import sys
import portprotonqt.themes.standart.styles as default_styles
import psutil
from portprotonqt.logger import get_logger
from portprotonqt.dialogs import AddGameDialog, FileExplorer
from portprotonqt.game_card import GameCard
from portprotonqt.animations import DetailPageAnimations
@@ -31,7 +30,6 @@ from portprotonqt.config_utils import (
clear_cache, read_auto_fullscreen_gamepad, save_auto_fullscreen_gamepad, read_rumble_config, save_rumble_config
)
from portprotonqt.localization import _, get_egs_language, read_metadata_translations
from portprotonqt.logger import get_logger
from portprotonqt.howlongtobeat_api import HowLongToBeat
from portprotonqt.downloader import Downloader
from portprotonqt.tray_manager import TrayManager
@@ -60,15 +58,7 @@ class MainWindow(QMainWindow):
self.is_exiting = False
selected_theme = read_theme_from_config()
self.current_theme_name = selected_theme
try:
self.theme = self.theme_manager.apply_theme(selected_theme)
except FileNotFoundError:
logger.warning(f"Тема '{selected_theme}' не найдена, применяется стандартная тема 'standart'")
self.theme = self.theme_manager.apply_theme("standart")
self.current_theme_name = "standart"
save_theme_to_config("standart")
if not self.theme:
self.theme = default_styles
self.theme = self.theme_manager.apply_theme(selected_theme)
self.tray_manager = TrayManager(self, app_name, self.current_theme_name)
self.card_width = read_card_size()
self.setWindowTitle(app_name)

View File

@@ -22,6 +22,7 @@ import websocket
import requests
import random
import base64
import glob
downloader = Downloader()
logger = get_logger(__name__)
@@ -265,10 +266,20 @@ def get_exiftool_data(game_exe):
logger.error(f"An unexpected error occurred in get_exiftool_data for {game_exe}: {e}")
return {}
def delete_cached_app_files(cache_dir: str, pattern: str):
"""Deletes cached files matching the given pattern in the cache directory."""
try:
for file_path in glob.glob(os.path.join(cache_dir, pattern)):
os.remove(file_path)
logger.info(f"Deleted cached file: {file_path}")
except Exception as e:
logger.error(f"Failed to delete cached files matching {pattern}: {e}")
def load_steam_apps_async(callback: Callable[[list], None]):
"""
Asynchronously loads the list of Steam applications, using cache if available.
Calls the callback with the list of apps.
Deletes cached app detail files when downloading a new steam_apps.json.
"""
cache_dir = get_cache_dir()
cache_tar = os.path.join(cache_dir, "games_appid.tar.xz")
@@ -295,6 +306,8 @@ def load_steam_apps_async(callback: Callable[[list], None]):
if os.path.exists(cache_tar):
os.remove(cache_tar)
logger.info("Archive %s deleted after extraction", cache_tar)
# Delete all cached app detail files (steam_app_*.json)
delete_cached_app_files(cache_dir, "steam_app_*.json")
steam_apps = data if isinstance(data, list) else []
logger.info("Loaded %d apps from archive", len(steam_apps))
callback(steam_apps)
@@ -325,11 +338,15 @@ def load_steam_apps_async(callback: Callable[[list], None]):
app_list_url = (
"https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/data/games_appid.tar.xz"
)
# Delete cached app detail files before re-downloading
delete_cached_app_files(cache_dir, "steam_app_*.json")
downloader.download_async(app_list_url, cache_tar, timeout=5, callback=process_tar)
else:
app_list_url = (
"https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/data/games_appid.tar.xz"
)
# Delete cached app detail files before downloading
delete_cached_app_files(cache_dir, "steam_app_*.json")
downloader.download_async(app_list_url, cache_tar, timeout=5, callback=process_tar)
def build_index(steam_apps):
@@ -427,6 +444,7 @@ def load_weanticheatyet_data_async(callback: Callable[[list], None]):
"""
Asynchronously loads the list of WeAntiCheatYet data, using cache if available.
Calls the callback with the list of anti-cheat data.
Deletes cached anti-cheat files when downloading a new anticheat_games.json.
"""
cache_dir = get_cache_dir()
cache_tar = os.path.join(cache_dir, "anticheat_games.tar.xz")
@@ -483,11 +501,15 @@ def load_weanticheatyet_data_async(callback: Callable[[list], None]):
app_list_url = (
"https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/data/anticheat_games.tar.xz"
)
# Delete cached anti-cheat files before re-downloading
delete_cached_app_files(cache_dir, "anticheat_*.json") # Adjust pattern if app-specific files are added
downloader.download_async(app_list_url, cache_tar, timeout=5, callback=process_tar)
else:
app_list_url = (
"https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/data/anticheat_games.tar.xz"
)
# Delete cached anti-cheat files before downloading
delete_cached_app_files(cache_dir, "anticheat_*.json") # Adjust pattern if app-specific files are added
downloader.download_async(app_list_url, cache_tar, timeout=5, callback=process_tar)
def build_weanticheatyet_index(anti_cheat_data):

View File

@@ -1,9 +1,9 @@
import importlib.util
import os
import ast
from portprotonqt.logger import get_logger
from PySide6.QtSvg import QSvgRenderer
from PySide6.QtGui import QIcon, QColor, QFontDatabase, QPixmap, QPainter
from portprotonqt.config_utils import save_theme_to_config, load_theme_metainfo
logger = get_logger(__name__)
@@ -15,6 +15,58 @@ THEMES_DIRS = [
os.path.join(os.path.dirname(os.path.abspath(__file__)), "themes")
]
# Запрещенные модули и функции
FORBIDDEN_MODULES = {
"os",
"subprocess",
"shutil",
"sys",
"socket",
"ctypes",
"pathlib",
"glob",
}
FORBIDDEN_FUNCTIONS = {
"exec",
"eval",
"open",
"__import__",
}
def check_theme_safety(theme_file: str) -> bool:
"""
Проверяет файл темы на наличие запрещённых модулей и функций.
Возвращает True, если файл безопасен, иначе False.
"""
has_errors = False
try:
with open(theme_file) as f:
content = f.read()
# Проверка на опасные импорты и функции
try:
tree = ast.parse(content)
for node in ast.walk(tree):
# Проверка импортов
if isinstance(node, ast.Import | ast.ImportFrom):
for name in node.names:
if name.name in FORBIDDEN_MODULES:
logger.error(f"Forbidden module '{name.name}' found in file {theme_file}")
has_errors = True
# Проверка вызовов функций
if isinstance(node, ast.Call):
if isinstance(node.func, ast.Name) and node.func.id in FORBIDDEN_FUNCTIONS:
logger.error(f"Forbidden function '{node.func.id}' found in file {theme_file}")
has_errors = True
except SyntaxError as e:
logger.error(f"Syntax error in file {theme_file}: {e}")
has_errors = True
except Exception as e:
logger.error(f"Failed to check theme safety for {theme_file}: {e}")
has_errors = True
return not has_errors
def list_themes():
"""
Возвращает список доступных тем (названий папок) из каталогов THEMES_DIRS.
@@ -66,7 +118,7 @@ def load_theme_fonts(theme_name):
break
if not fonts_folder or not os.path.exists(fonts_folder):
logger.error(f"Папка fonts не найдена для темы '{theme_name}'")
logger.error(f"Fonts folder not found for theme '{theme_name}'")
return
for filename in os.listdir(fonts_folder):
@@ -75,9 +127,9 @@ def load_theme_fonts(theme_name):
font_id = QFontDatabase.addApplicationFont(font_path)
if font_id != -1:
families = QFontDatabase.applicationFontFamilies(font_id)
logger.info(f"Шрифт {filename} успешно загружен: {families}")
logger.info(f"Font {filename} successfully loaded: {families}")
else:
logger.error(f"Ошибка загрузки шрифта: {filename}")
logger.error(f"Error loading font: {filename}")
def load_logo():
logo_path = None
@@ -90,7 +142,7 @@ def load_logo():
if file_extension == ".svg":
renderer = QSvgRenderer(logo_path)
if not renderer.isValid():
logger.error(f"Ошибка загрузки SVG логотипа: {logo_path}")
logger.error(f"Error loading SVG logo: {logo_path}")
return None
pixmap = QPixmap(128, 128)
pixmap.fill(QColor(0, 0, 0, 0))
@@ -109,37 +161,42 @@ class ThemeWrapper:
self.custom_theme = custom_theme
self.metainfo = metainfo or {}
self.screenshots = load_theme_screenshots(self.metainfo.get("name", ""))
self._default_theme = None # Lazy-loaded default theme
def __getattr__(self, name):
if hasattr(self.custom_theme, name):
return getattr(self.custom_theme, name)
import portprotonqt.themes.standart.styles as default_styles
return getattr(default_styles, name)
if self._default_theme is None:
self._default_theme = load_theme("standart") # Dynamically load standard theme
return getattr(self._default_theme, name)
def load_theme(theme_name):
"""
Динамически загружает модуль стилей выбранной темы и метаинформацию.
Если выбрана стандартная тема, импортируется оригинальный styles.py.
Все темы, включая стандартную, проходят проверку безопасности.
Для кастомных тем возвращается обёртка, которая подставляет недостающие атрибуты.
"""
if theme_name == "standart":
import portprotonqt.themes.standart.styles as default_styles
return default_styles
for themes_dir in THEMES_DIRS:
theme_folder = os.path.join(themes_dir, theme_name)
styles_file = os.path.join(theme_folder, "styles.py")
if os.path.exists(styles_file):
# Проверяем безопасность темы перед загрузкой
if not check_theme_safety(styles_file):
logger.error(f"Theme '{theme_name}' is unsafe, falling back to 'standart'")
raise FileNotFoundError(f"Theme '{theme_name}' contains forbidden modules or functions")
spec = importlib.util.spec_from_file_location("theme_styles", styles_file)
if spec is None or spec.loader is None:
continue
custom_theme = importlib.util.module_from_spec(spec)
spec.loader.exec_module(custom_theme)
if theme_name == "standart":
return custom_theme
meta = load_theme_metainfo(theme_name)
wrapper = ThemeWrapper(custom_theme, metainfo=meta)
wrapper.screenshots = load_theme_screenshots(theme_name)
return wrapper
raise FileNotFoundError(f"Файл стилей не найден для темы '{theme_name}'")
raise FileNotFoundError(f"Styles file not found for theme '{theme_name}'")
class ThemeManager:
"""
@@ -166,12 +223,18 @@ class ThemeManager:
:param theme_name: Имя темы.
:return: Загруженный модуль темы (или обёртка).
"""
theme_module = load_theme(theme_name)
try:
theme_module = load_theme(theme_name)
except FileNotFoundError:
logger.warning(f"Theme '{theme_name}' not found or unsafe, applying standard theme 'standart'")
theme_module = load_theme("standart")
theme_name = "standart"
save_theme_to_config("standart")
load_theme_fonts(theme_name)
self.current_theme_name = theme_name
self.current_theme_module = theme_module
save_theme_to_config(theme_name)
logger.info(f"Тема '{theme_name}' успешно применена")
logger.info(f"Theme '{theme_name}' successfully applied")
return theme_module
def get_icon(self, icon_name, theme_name=None, as_path=False):
@@ -226,7 +289,7 @@ class ThemeManager:
# Если иконка всё равно не найдена
if not icon_path or not os.path.exists(icon_path):
logger.error(f"Предупреждение: иконка '{icon_name}' не найдена")
logger.error(f"Warning: icon '{icon_name}' not found")
return QIcon() if not as_path else None
if as_path:

View File

@@ -9,7 +9,6 @@ from PySide6.QtGui import QIcon, QAction
from PySide6.QtCore import QTimer
from portprotonqt.logger import get_logger
from portprotonqt.theme_manager import ThemeManager
import portprotonqt.themes.standart.styles as default_styles
from portprotonqt.localization import _
from portprotonqt.config_utils import read_favorites, read_theme_from_config, save_theme_to_config
from portprotonqt.dialogs import GameLaunchDialog
@@ -31,15 +30,7 @@ class TrayManager:
self.theme_manager = ThemeManager()
selected_theme = read_theme_from_config()
self.current_theme_name = selected_theme
try:
self.theme = self.theme_manager.apply_theme(selected_theme)
except FileNotFoundError:
logger.warning(f"Тема '{selected_theme}' не найдена, применяется стандартная тема 'standart'")
self.theme = self.theme_manager.apply_theme("standart")
self.current_theme_name = "standart"
save_theme_to_config("standart")
if not self.theme:
self.theme = default_styles
self.theme = self.theme_manager.apply_theme(selected_theme)
self.main_window = main_window
self.tray_icon = QSystemTrayIcon(self.main_window)

View File

@@ -5,21 +5,25 @@
"lockFileMaintenance": {
"enabled": true
},
"pre-commit": {
"enabled": true
},
"packageRules": [
{
"matchUpdateTypes": ["minor", "patch"],
"automerge": true
},
{
"matchDatasources": ["python-version"],
"enabled": false
"matchFileNames": [".gitea/workflows/build.yml"],
"enabled": false,
"description": "Disabled because download-artifact@v4 is not working"
},
{
"matchFileNames": [".python-version"],
"enabled": false
"enabled": false,
},
{
"matchManagers": ["github-actions", "pre-commit", "poetry"],
"matchManagers": ["poetry", "pyenv"],
"enabled": false
},
{
@@ -29,9 +33,14 @@
"groupName": "Python dependencies"
},
{
"matchPackageNames": ["numpy", "setuptools"],
"matchPackageNames": ["numpy", "setuptools", "python"],
"enabled": false,
"description": "Disabled due to Python 3.10 incompatibility with numpy>=2.3.2 (requires Python>=3.11)"
}
},
{
"matchDatasources": ["github-runners", "python-version"],
"enabled": false,
"description": "Prevent Renovate from updating runs-on to unsupported ubuntu-24.04"
},
]
}