From b459b95950c8557a8f3d3f2d67f0a4420759f503 Mon Sep 17 00:00:00 2001 From: Srecko Skocilic Date: Sat, 2 May 2026 03:28:01 +0200 Subject: [PATCH 1/7] - The comma-separated return creates a 7-element tuple instead of comparing two tuples, every equality check returns True - master -> main branch fix - prepare_trash now calls self.move_to_trash instead of self.delete, so plugin subclasses won't permanently delete files when the user expects trashing - NotImplementedError - unrecognized platforms fail fast with a clear message instead of a cryptic NameError - removed shadowing basename import - get_column_widths uses range so plugin-added columns get their widths saved/restored --- build.py | 8 ++++---- src/main/python/fman/__init__.py | 2 ++ src/main/python/fman/fs.py | 2 +- src/main/python/fman/impl/model/worker.py | 6 +++--- src/main/python/fman/impl/widgets.py | 2 +- .../resources/base/Plugins/Core/core/commands/__init__.py | 2 +- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/build.py b/build.py index 60255c1a..f9988e72 100644 --- a/build.py +++ b/build.py @@ -96,7 +96,7 @@ def release(): settings_path, next_version + snapshot_suffix, 'Bump version for next development iteration' ) - git('push', '-u', 'origin', 'master') + git('push', '-u', 'origin', 'main') try: git('push', 'origin', release_tag) try: @@ -106,7 +106,7 @@ def release(): ' git pull\n' ' git checkout %s\n' ' python build.py release\n' - ' git checkout master\n\n' + ' git checkout main\n\n' 'on the other OSs now, then come back here and do:' '\n\n' ' python build.py post_release\n' @@ -117,7 +117,7 @@ def release(): raise except: git('revert', '--no-edit', revision_before + '..HEAD' ) - git('push', '-u', 'origin', 'master') + git('push', '-u', 'origin', 'main') revision_before = git('rev-parse', 'HEAD').rstrip() raise except: @@ -149,7 +149,7 @@ def post_release(): create_cloudfront_invalidation(cloudfront_items_to_invalidate) record_release_on_server() upload_core_to_github() - git('checkout', 'master') + git('checkout', 'main') def _prompt_for_next_version(release_version): next_version = _get_suggested_next_version(release_version) diff --git a/src/main/python/fman/__init__.py b/src/main/python/fman/__init__.py index ab1f58d1..eefe7c12 100644 --- a/src/main/python/fman/__init__.py +++ b/src/main/python/fman/__init__.py @@ -41,6 +41,8 @@ DATA_DIRECTORY = expanduser('~/Library/Application Support/fman') elif PLATFORM == 'Linux': DATA_DIRECTORY = expanduser('~/.config/fman') +else: + raise NotImplementedError('Unsupported platform: %s' % PLATFORM) class ApplicationCommand: def __init__(self, window): diff --git a/src/main/python/fman/fs.py b/src/main/python/fman/fs.py index 2cf8f00f..d020a0a7 100644 --- a/src/main/python/fman/fs.py +++ b/src/main/python/fman/fs.py @@ -146,7 +146,7 @@ def prepare_trash(self, path): raise self._operation_not_implemented() return [Task( 'Deleting ' + path.rsplit('/', 1)[-1], - fn=self.delete, args=(path,), size=1 + fn=self.move_to_trash, args=(path,), size=1 )] def touch(self, path): raise self._operation_not_implemented() diff --git a/src/main/python/fman/impl/model/worker.py b/src/main/python/fman/impl/model/worker.py index 51066233..df74de27 100644 --- a/src/main/python/fman/impl/model/worker.py +++ b/src/main/python/fman/impl/model/worker.py @@ -21,7 +21,7 @@ def submit(self, priority, fn, *args, **kwargs): with self._shutdown_lock: if self._shutdown: return - self._queue.put(WorkItem(priority, fn, *args, *kwargs)) + self._queue.put(WorkItem(priority, fn, *args, **kwargs)) def shutdown(self): with self._shutdown_lock: self._shutdown = True @@ -56,7 +56,7 @@ def __lt__(self, other): return NotImplemented def __eq__(self, other): try: - return self._fn, self._args, self._kwargs, self._priority == \ - other._fn, other._args, other._kwargs, other._priority + return (self._fn, self._args, self._kwargs, self._priority) == \ + (other._fn, other._args, other._kwargs, other._priority) except AttributeError: return NotImplemented \ No newline at end of file diff --git a/src/main/python/fman/impl/widgets.py b/src/main/python/fman/impl/widgets.py index c92c08fc..12a8fe24 100644 --- a/src/main/python/fman/impl/widgets.py +++ b/src/main/python/fman/impl/widgets.py @@ -155,7 +155,7 @@ def get_sort_column(self): return column, ascending @run_in_main_thread def get_column_widths(self): - return [self._file_view.columnWidth(i) for i in (0, 1)] + return [self._file_view.columnWidth(i) for i in range(self._model.columnCount())] @run_in_main_thread def set_column_widths(self, column_widths): num_columns = self._model.columnCount() diff --git a/src/main/resources/base/Plugins/Core/core/commands/__init__.py b/src/main/resources/base/Plugins/Core/core/commands/__init__.py index 319553d4..644397e8 100644 --- a/src/main/resources/base/Plugins/Core/core/commands/__init__.py +++ b/src/main/resources/base/Plugins/Core/core/commands/__init__.py @@ -17,7 +17,7 @@ from io import UnsupportedOperation from itertools import chain from os import strerror -from os.path import basename, pardir +from os.path import pardir from pathlib import PurePath from PyQt5.QtCore import QUrl from PyQt5.QtGui import QDesktopServices From 98a8c0eb433e69f52321d8639016feeb0c3ac76f Mon Sep 17 00:00:00 2001 From: Srecko Skocilic Date: Sat, 2 May 2026 03:34:28 +0200 Subject: [PATCH 2/7] fix failing tests --- src/main/resources/base/Plugins/Core/core/tests/fs/test_zip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/base/Plugins/Core/core/tests/fs/test_zip.py b/src/main/resources/base/Plugins/Core/core/tests/fs/test_zip.py index 837f2bc2..a68a38d6 100644 --- a/src/main/resources/base/Plugins/Core/core/tests/fs/test_zip.py +++ b/src/main/resources/base/Plugins/Core/core/tests/fs/test_zip.py @@ -351,7 +351,7 @@ def _read_directory(self, dir_path): child_contents = self._read_directory(child) else: child_contents = child.read_text() - result[child.name] = child_contents + result[normalize('NFC', child.name)] = child_contents return result def _expect_zip_contents(self, contents, zip_file_path): with TemporaryDirectory() as tmp_dir: From 8cf09613cc230d23046428fc31f161b238ac9b3c Mon Sep 17 00:00:00 2001 From: Srecko Skocilic Date: Sat, 2 May 2026 03:43:47 +0200 Subject: [PATCH 3/7] all pending plugin errors are now shown at startup instead of just the first one --- src/main/python/fman/impl/plugins/error.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/python/fman/impl/plugins/error.py b/src/main/python/fman/impl/plugins/error.py index 8b19d965..c6797555 100644 --- a/src/main/python/fman/impl/plugins/error.py +++ b/src/main/python/fman/impl/plugins/error.py @@ -46,8 +46,8 @@ def handle_system_exit(self, code=0): self._app.exit(code) def on_main_window_shown(self, main_window): self._main_window = main_window - if self._pending_error_messages: - self._main_window.show_alert(self._pending_error_messages[0]) + for message in self._pending_error_messages: + self._main_window.show_alert(message) def _get_plugin_traceback(self, exc): if isinstance(exc, ThemeError): return exc.description From 064b5f0eb887c97202e3f80b7973715a33d7aa7d Mon Sep 17 00:00:00 2001 From: Srecko Skocilic Date: Sat, 2 May 2026 03:49:46 +0200 Subject: [PATCH 4/7] =?UTF-8?q?=20=20-=20command=5Fregistry.py=20=E2=80=94?= =?UTF-8?q?=20=5Fset=5Fcontext=20now=20uses=20try/finally=20so=20cm.=5F=5F?= =?UTF-8?q?exit=5F=5F=20always=20runs,=20even=20on=20exception.=20=20=20-?= =?UTF-8?q?=20util/qt/=5F=5Finit=5F=5F.py=20=E2=80=94=20Added=20missing=20?= =?UTF-8?q?c=5Fvoid=5Fp=20import=20from=20ctypes,=20fixing=20a=20macOS=20r?= =?UTF-8?q?untime=20crash.=20=20=20-=20table.py=20=E2=80=94=20Fixed=20off-?= =?UTF-8?q?by-one:=20bounds=20check=20now=20rejects=20len=20+=201=20correc?= =?UTF-8?q?tly.=20=20=20-=20widgets.py=20=E2=80=94=20Added=20null=20guard?= =?UTF-8?q?=20on=20=5Fmain=5Fwindow=20before=20accessing=20it=20in=20state?= =?UTF-8?q?=20change=20handler.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/python/fman/impl/model/table.py | 2 +- src/main/python/fman/impl/plugins/command_registry.py | 8 +++++--- src/main/python/fman/impl/util/qt/__init__.py | 3 ++- src/main/python/fman/impl/widgets.py | 2 +- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/python/fman/impl/model/table.py b/src/main/python/fman/impl/model/table.py index 33fe1c80..c1d17ff6 100644 --- a/src/main/python/fman/impl/model/table.py +++ b/src/main/python/fman/impl/model/table.py @@ -151,7 +151,7 @@ def insert(self, rows, first_rownum): new_keys = {row.key: first_rownum + i for i, row in enumerate(rows)} with self._lock: # Perform this check here, once we have the lock: - if first_rownum < 0 or first_rownum > len(self._rows) + 1: + if first_rownum < 0 or first_rownum > len(self._rows): raise ValueError('Invalid first_rownum: %d' % first_rownum) num_rows = len(rows) for row in self._rows[first_rownum:]: diff --git a/src/main/python/fman/impl/plugins/command_registry.py b/src/main/python/fman/impl/plugins/command_registry.py index e7204763..7bb1a0e6 100644 --- a/src/main/python/fman/impl/plugins/command_registry.py +++ b/src/main/python/fman/impl/plugins/command_registry.py @@ -153,9 +153,11 @@ def _set_context(self, pane, file_under_cursor=_DEFAULT): if file_under_cursor is not self._DEFAULT: cm = pane._override_file_under_cursor(file_under_cursor) cm.__enter__() - yield - if file_under_cursor is not self._DEFAULT: - cm.__exit__(None, None, None) + try: + yield + finally: + if file_under_cursor is not self._DEFAULT: + cm.__exit__(None, None, None) def _get_default_aliases(cmd_class): return re.sub(r'([a-z])([A-Z])', r'\1 \2', cmd_class.__name__)\ diff --git a/src/main/python/fman/impl/util/qt/__init__.py b/src/main/python/fman/impl/util/qt/__init__.py index 9d77ee96..8baddbad 100644 --- a/src/main/python/fman/impl/util/qt/__init__.py +++ b/src/main/python/fman/impl/util/qt/__init__.py @@ -16,8 +16,9 @@ def disable_window_animations_mac(window): # penalties and leads to subtle changes in behaviour. We therefore wait for # the Show event: def eventFilter(target, event): + from ctypes import c_void_p from objc import objc_object - view = objc_object(c_void_p=int(target.winId())) + view = objc_object(c_void_p=c_void_p(int(target.winId()))) NSWindowAnimationBehaviorNone = 2 view.window().setAnimationBehavior_(NSWindowAnimationBehaviorNone) FilterEventOnce(window, QEvent.Show, eventFilter) diff --git a/src/main/python/fman/impl/widgets.py b/src/main/python/fman/impl/widgets.py index 12a8fe24..5a591d98 100644 --- a/src/main/python/fman/impl/widgets.py +++ b/src/main/python/fman/impl/widgets.py @@ -37,7 +37,7 @@ def exit(self, returnCode=0): def set_style_sheet(self, stylesheet): self.setStyleSheet(stylesheet) def _on_state_changed(self, new_state): - if new_state == Qt.ApplicationActive: + if new_state == Qt.ApplicationActive and self._main_window is not None: for pane in self._main_window.get_panes(): pane.reload() From 93f580e40940ccf1b539a398466bd1b05b1eae11 Mon Sep 17 00:00:00 2001 From: Srecko Skocilic Date: Sat, 2 May 2026 03:50:56 +0200 Subject: [PATCH 5/7] =?UTF-8?q?=20=20-=20util/path.py=20=E2=80=94=20normal?= =?UTF-8?q?ize=20now=20loops=20until=20all=20..=20segments=20are=20resolve?= =?UTF-8?q?d,=20so=20a/b/c/../../d=20correctly=20becomes=20a/d.=20=20=20-?= =?UTF-8?q?=20session.py=20=E2=80=94=20Removed=20dead=20=5Fget=5Fstartup?= =?UTF-8?q?=5Fmessage=20method=20(duplicated=20by=20=5Fshow=5Fstartup=5Fme?= =?UTF-8?q?ssages)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/python/fman/impl/session.py | 7 ------- src/main/python/fman/impl/util/path.py | 5 ++++- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/main/python/fman/impl/session.py b/src/main/python/fman/impl/session.py index 58278245..18144e70 100644 --- a/src/main/python/fman/impl/session.py +++ b/src/main/python/fman/impl/session.py @@ -68,13 +68,6 @@ def _show_startup_messages(self, main_window): 'Updated to v%s. ' \ 'Changelog' % self._fman_version main_window.show_status_message(status_message, timeout_secs=5) - def _get_startup_message(self): - previous_version = self._settings.get('fman_version', None) - if not previous_version or previous_version == self._fman_version: - return 'v%s ready.' % self._fman_version - return 'Updated to v%s. ' \ - 'Changelog' \ - % self._fman_version def _init_panes(self, panes, pane_infos, paths_on_cmdline): with ThreadPoolExecutor(max_workers=len(panes)) as executor: futures = [ diff --git a/src/main/python/fman/impl/util/path.py b/src/main/python/fman/impl/util/path.py index b5ffff8a..7105975f 100644 --- a/src/main/python/fman/impl/util/path.py +++ b/src/main/python/fman/impl/util/path.py @@ -34,5 +34,8 @@ def normalize(path_): if path_ == '.': path_ = '' # Resolve a/../b - path_ = re.subn(r'(^|/)([^/]+)/\.\.(?:$|/)', r'\1', path_)[0] + while True: + path_, count = re.subn(r'(^|/)([^/]+)/\.\.(?:$|/)', r'\1', path_) + if not count: + break return path_.rstrip('/') \ No newline at end of file From 74f1ba7cee0e48c29eaab9b999811bc2df97124e Mon Sep 17 00:00:00 2001 From: Srecko Skocilic Date: Sun, 3 May 2026 06:30:22 +0200 Subject: [PATCH 6/7] bugfix --- src/main/resources/base/Plugins/Core/core/commands/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/base/Plugins/Core/core/commands/__init__.py b/src/main/resources/base/Plugins/Core/core/commands/__init__.py index 644397e8..b6c88f53 100644 --- a/src/main/resources/base/Plugins/Core/core/commands/__init__.py +++ b/src/main/resources/base/Plugins/Core/core/commands/__init__.py @@ -1730,7 +1730,7 @@ def on_command(self, command_name, args): except (KeyError, ValueError): return None if scheme == 'file://': - new_scheme = _get_handler_for_archive(basename(path)) + new_scheme = _get_handler_for_archive(basename(url)) if new_scheme: try: if is_dir(url): From 8bad22ba1159099565b51e00d613df6fbf346c83 Mon Sep 17 00:00:00 2001 From: Srecko Skocilic Date: Fri, 8 May 2026 19:36:12 +0200 Subject: [PATCH 7/7] Fix security vulnerabilities found by code quality scan - os_.py: whitelist allowed Popen kwargs to prevent shell injection via user JSON settings - os_.py: return empty string when /etc/os-release has no NAME= entry - github.py: add timeout=30 to urlopen and requests.get preventing GUI hangs and socket leaks - post-commit: replace hardcoded API secret with $FMAN_API_SECRET env var - base.txt: fetch fbs dependency over HTTPS instead of HTTP - windows.json: replace dead Symantec timestamp server with DigiCert - fman.repo: enable gpgcheck and add gpgkey for RPM package verification - commands/__init__.py: reject plugin names containing path separators or '..' - build_impl/__init__.py: validate record_release_url uses HTTPS, add request timeout, chmod check=True, exclude .git from cleanup --- bin/post-commit | 6 +++++- requirements/base.txt | 2 +- src/build/python/build_impl/__init__.py | 11 +++++++---- src/build/settings/windows.json | 2 +- .../base/Plugins/Core/core/commands/__init__.py | 2 ++ src/main/resources/base/Plugins/Core/core/github.py | 5 +++-- src/main/resources/base/Plugins/Core/core/os_.py | 4 ++++ src/repo/fedora/fman.repo | 3 ++- 8 files changed, 25 insertions(+), 10 deletions(-) diff --git a/bin/post-commit b/bin/post-commit index 8e8e7da7..88f309f4 100755 --- a/bin/post-commit +++ b/bin/post-commit @@ -5,11 +5,15 @@ set -e URL=https://fman.io/api/record-commit/ +if [ -z "${FMAN_API_SECRET:-}" ]; then + exit 0 +fi + message_tmp_file=`mktemp` git log --pretty=format:%B -n1 > ${message_tmp_file} sha=`git log --pretty=format:%H -n1` date=`git log --pretty=format:%cd --date=iso-strict -n1` -curl --data-urlencode secret=k92XhhhmOf8rD7QJ --data-urlencode sha=${sha} \ +curl --fail --silent --data-urlencode secret=${FMAN_API_SECRET} --data-urlencode sha=${sha} \ --data-urlencode date=${date} --data-urlencode message@${message_tmp_file} \ ${URL} rm ${message_tmp_file} \ No newline at end of file diff --git a/requirements/base.txt b/requirements/base.txt index a2bddc2e..513111af 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,4 +1,4 @@ -fbs[sentry] @ http://build-system.fman.io/pro/b5aab865-bd29-4f23-992c-0eb4f3a24f33/0.9.4 +fbs[sentry] @ https://build-system.fman.io/pro/b5aab865-bd29-4f23-992c-0eb4f3a24f33/0.9.4 PyQt5==5.15.11 PyInstaller==6.19.0 rsa==4.9 diff --git a/src/build/python/build_impl/__init__.py b/src/build/python/build_impl/__init__.py index 086f20aa..5b58965f 100644 --- a/src/build/python/build_impl/__init__.py +++ b/src/build/python/build_impl/__init__.py @@ -105,7 +105,7 @@ def upload_core_to_github(): ssh_key = path('${core_plugin_ssh_key}') if not is_windows(): # Prevent clone failing due to lacking access restrictions: - run(['chmod', '600', ssh_key]) + run(['chmod', '600', ssh_key], check=True) with TemporaryDirectory() as tmp_dir: cwd_before = getcwd() chdir(tmp_dir) @@ -118,7 +118,7 @@ def upload_core_to_github(): if not line.startswith('#') and line.rstrip() } for name in listdir(tmp_dir): - if name not in extra_files: + if name not in extra_files and name != '.git': if isdir(name): rmtree(name) else: @@ -146,8 +146,11 @@ def upload_core_to_github(): def record_release_on_server(): import requests - response = requests.post(SETTINGS['record_release_url'], { + url = SETTINGS['record_release_url'] + if not url.startswith('https://'): + raise ValueError('record_release_url must use HTTPS') + response = requests.post(url, { 'secret': SETTINGS['server_api_secret'], 'version': SETTINGS['version'] - }) + }, timeout=30) response.raise_for_status() \ No newline at end of file diff --git a/src/build/settings/windows.json b/src/build/settings/windows.json index 267f4713..bb66039d 100644 --- a/src/build/settings/windows.json +++ b/src/build/settings/windows.json @@ -4,5 +4,5 @@ "win32com.shell.shellcon", "win32gui", "winpty", "win32wnet" ], "windows_sign_pass": "Tu4suttmdpn", - "windows_sign_server": "http://sha256timestamp.ws.symantec.com/sha256/timestamp" + "windows_sign_server": "http://timestamp.digicert.com" } \ No newline at end of file diff --git a/src/main/resources/base/Plugins/Core/core/commands/__init__.py b/src/main/resources/base/Plugins/Core/core/commands/__init__.py index b6c88f53..9d4c83b3 100644 --- a/src/main/resources/base/Plugins/Core/core/commands/__init__.py +++ b/src/main/resources/base/Plugins/Core/core/commands/__init__.py @@ -1427,6 +1427,8 @@ def _get_matching_repos(self, query): description=repo.description ) def _install_plugin(self, name, zipball_contents): + if os.sep in name or '/' in name or '..' in name: + raise ValueError('Invalid plugin name: %s' % name) os.makedirs(_THIRDPARTY_PLUGINS_DIR, exist_ok=True) dest_dir = os.path.join(_THIRDPARTY_PLUGINS_DIR, name) dest_dir_url = as_url(dest_dir) diff --git a/src/main/resources/base/Plugins/Core/core/github.py b/src/main/resources/base/Plugins/Core/core/github.py index d0c6b280..6db49e12 100644 --- a/src/main/resources/base/Plugins/Core/core/github.py +++ b/src/main/resources/base/Plugins/Core/core/github.py @@ -73,13 +73,14 @@ def _get_json(url): def _get(url): try: - return urlopen(url).read() + with urlopen(url, timeout=30) as resp: + return resp.read() except HTTPError: raise except URLError: # Fallback: Some users get "SSL: CERTIFICATE_VERIFY_FAILED" for urlopen. try: - response = requests.get(url) + response = requests.get(url, timeout=30) except RequestException as e: raise URLError(e.__class__.__name__) if response.status_code != 200: diff --git a/src/main/resources/base/Plugins/Core/core/os_.py b/src/main/resources/base/Plugins/Core/core/os_.py index 00205e99..6a1287f6 100644 --- a/src/main/resources/base/Plugins/Core/core/os_.py +++ b/src/main/resources/base/Plugins/Core/core/os_.py @@ -85,8 +85,11 @@ def _is_ubuntu(): except FileNotFoundError: return False +_ALLOWED_POPEN_KEYS = {'args', 'cwd', 'env', 'startupinfo'} + def _run_app_from_setting(app, curr_dir): popen_kwargs = strformat_dict_values(app, {'curr_dir': curr_dir}) + popen_kwargs = {k: v for k, v in popen_kwargs.items() if k in _ALLOWED_POPEN_KEYS} Popen(**popen_kwargs) def _is_gnome_based(): @@ -100,6 +103,7 @@ def _get_os_release_name(): if line.startswith('NAME='): name = line[len('NAME='):] return name.strip('"') + return '' _FOCUS_PREVENTION_LEVEL = \ '/org/compiz/profiles/unity/plugins/core/focus-prevention-level' \ No newline at end of file diff --git a/src/repo/fedora/fman.repo b/src/repo/fedora/fman.repo index 06cbb5fc..1354f2b9 100644 --- a/src/repo/fedora/fman.repo +++ b/src/repo/fedora/fman.repo @@ -2,4 +2,5 @@ name=fman baseurl=https://download.fman.io/rpm enabled=1 -gpgcheck=0 \ No newline at end of file +gpgcheck=1 +gpgkey=https://download.fman.io/rpm/public.gpg \ No newline at end of file