From a609f55879e859ca936394543d60b61b5aa925d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20B=C3=ADcha?= Date: Mon, 21 Aug 2023 21:33:43 +0100 Subject: Import xdg-desktop-portal_1.17.0-1.debian.tar.xz [dgit import tarball xdg-desktop-portal 1.17.0-1 xdg-desktop-portal_1.17.0-1.debian.tar.xz] --- changelog | 620 +++++++++++++++++++++ control | 101 ++++ copyright | 67 +++ gbp.conf | 7 + not-installed | 1 + ...open-uri-Fix-a-presumably-copy-paste-typo.patch | 24 + ...tivation-token-to-the-FileManager1-interf.patch | 24 + patches/series | 3 + ...ues-from-read-being-variants-inside-varia.patch | 124 +++++ rules | 35 ++ salsa-ci.yml | 3 + source/format | 1 + tests/control | 5 + tests/gnome-desktop-testing | 12 + upstream/metadata | 6 + watch | 9 + xdg-desktop-portal-dev.doc-base | 8 + xdg-desktop-portal-dev.install | 4 + xdg-desktop-portal-tests.install | 2 + xdg-desktop-portal.docs | 1 + xdg-desktop-portal.install | 8 + xdg-desktop-portal.lintian-overrides | 2 + xdg-desktop-portal.postinst | 11 + 23 files changed, 1078 insertions(+) create mode 100644 changelog create mode 100644 control create mode 100644 copyright create mode 100644 gbp.conf create mode 100644 not-installed create mode 100644 patches/open-uri-Fix-a-presumably-copy-paste-typo.patch create mode 100644 patches/open-uri-Pass-activation-token-to-the-FileManager1-interf.patch create mode 100644 patches/series create mode 100644 patches/settings-Fix-values-from-read-being-variants-inside-varia.patch create mode 100755 rules create mode 100644 salsa-ci.yml create mode 100644 source/format create mode 100644 tests/control create mode 100755 tests/gnome-desktop-testing create mode 100644 upstream/metadata create mode 100644 watch create mode 100644 xdg-desktop-portal-dev.doc-base create mode 100644 xdg-desktop-portal-dev.install create mode 100644 xdg-desktop-portal-tests.install create mode 100644 xdg-desktop-portal.docs create mode 100644 xdg-desktop-portal.install create mode 100644 xdg-desktop-portal.lintian-overrides create mode 100644 xdg-desktop-portal.postinst diff --git a/changelog b/changelog new file mode 100644 index 0000000..93861a8 --- /dev/null +++ b/changelog @@ -0,0 +1,620 @@ +xdg-desktop-portal (1.17.0-1) experimental; urgency=medium + + * Team upload + * New upstream release + - Fixes portal delay with x-d-p-gnome 44 and non-GNOME desktops + (LP: #2013116) (Closes: #1032584) + * debian/control: Build-Depend on python3-dbusmock & python3-pytest + for build tests + * Drop all patches: applied in new release + * Cherry-pick 3 bug fixes from upstream repo + + -- Jeremy Bícha Mon, 21 Aug 2023 16:33:43 -0400 + +xdg-desktop-portal (1.16.0-3) unstable; urgency=medium + + * d/patches: Update to upstream 1.16 branch commit 1.16.0-17-g2a219279 + - Deal better with backends that are non-functional in desktop + environments other than the one they are intended for + - Improve handling of incorrect method calls from applications + - Extract app IDs from more systemd scope/slice/service names + - Update security support status in README + - Translation updates: be, gl, ka, oc, tr + * Update standards version to 4.6.2 (no changes needed) + * Remove version constraints unnecessary since Debian 11 + * d/xdp.lintian-overrides: Silence some false positives (#1031037) + + -- Simon McVittie Sat, 05 Aug 2023 15:33:04 +0100 + +xdg-desktop-portal (1.16.0-2) unstable; urgency=medium + + * d/patches: Add post-release bug fixes from upstream + - d/p/file-chooser-Set-writable-flag-correctly.patch: + Don't allow writing to files if not requested + - d/p/tests-List-lists-of-sources-one-per-line-in-alphabetical-.patch, + d/p/tests-Add-missing-dependency-on-permission-store-GDBus-he.patch: + Fix a race condition that was breaking bullseye-backports builds + + -- Simon McVittie Tue, 10 Jan 2023 11:31:45 +0000 + +xdg-desktop-portal (1.16.0-1) unstable; urgency=medium + + * New upstream release + * d/copyright: Update + * d/patches: Drop patches, included in the new release + * d/upstream/metadata: Add Security-Contact + * d/not-installed: Skip installation of an unnecessary file + + -- Simon McVittie Tue, 13 Dec 2022 22:52:21 +0000 + +xdg-desktop-portal (1.15.0-3) unstable; urgency=medium + + * d/watch: Adapt to Github web page changes + * d/control: Canonicalize case of Multi-Arch field + * d/p/meson-In-installed-tests-invoke-test-portals-once-per-por.patch: + Update patch to the version that was applied upstream + * d/p/test-portals-Include-utils-even-if-libportal-is-unavailab.patch, + d/p/tests-Don-t-apply-Meson-s-strict-TAP-parsing-for-older-GL.patch, + d/p/tests-Include-src-glib-backports.h-where-needed.patch, + d/p/glib-backports-Declare-stub-function-as-inline.patch: + Add patches from upstream needed for the Debian 11 backport + * d/patches: Add selected bug fixes from upstream git + - Move xdg-permission-store to the systemd slice appropriate for + session services used to support applications + - Improve systemd service management so our services do not linger after + graphical-session.target has exited + - Documentation fixes + - Make o.fd.portal.Screenshot.Version mirror + o.fd.portal.impl.Screenshot.Version so that clients can discover + whether the backend implements PickColor() + - Add a missing Qt type annotation to the Request.Response signal + * d/patches: Add localization updates from upstream + * Build-/test-depend on dbus-daemon instead of dbus + + -- Simon McVittie Tue, 22 Nov 2022 10:38:32 +0000 + +xdg-desktop-portal (1.15.0-2) unstable; urgency=medium + + * Release to unstable. + xdg-desktop-portal-gnome needs features of this version, and a 1.16.0 + stable release is intended to happen before the freeze. + * d/control: Drop compatibility with old libgdk-pixbuf2.0-dev package. + libgdk-pixbuf-2.0-dev has been available since bullseye. + + -- Simon McVittie Thu, 15 Sep 2022 19:20:27 +0100 + +xdg-desktop-portal (1.15.0-1) experimental; urgency=medium + + * New upstream development release + * Switch to experimental branch + - d/watch: Watch for development releases + - d/gbp.conf, d/control: Use debian/experimental packaging branch + * d/control, d/rules: Build using Meson + * Add patch to split up installed-tests coverage for test-portals. + This will make it easier to distinguish between tests that are stable + when run as an autopkgtest, and tests that are not. + + -- Simon McVittie Sun, 14 Aug 2022 21:08:48 +0100 + +xdg-desktop-portal (1.14.6-1) unstable; urgency=medium + + * New upstream stable release + + -- Simon McVittie Thu, 04 Aug 2022 09:17:19 +0100 + +xdg-desktop-portal (1.14.5-1) unstable; urgency=medium + + * New upstream stable release + * Standards-Version: 4.6.1 (no changes required) + + -- Simon McVittie Tue, 19 Jul 2022 18:58:07 +0100 + +xdg-desktop-portal (1.14.4-1) unstable; urgency=medium + + * New upstream stable release + + -- Simon McVittie Thu, 05 May 2022 15:14:29 +0100 + +xdg-desktop-portal (1.14.3-1) unstable; urgency=medium + + * New upstream stable release + + -- Simon McVittie Fri, 15 Apr 2022 16:30:43 +0100 + +xdg-desktop-portal (1.14.2-1) unstable; urgency=medium + + * New upstream release + * Drop patches, applied upstream + + -- Simon McVittie Thu, 31 Mar 2022 12:11:22 +0100 + +xdg-desktop-portal (1.14.1-2) unstable; urgency=medium + + * d/x-d-p.postinst: Clean up enabled state of x-d-p.service on upgrades. + When we upgrade from 1.14.0 to 1.14.1, we want to remove + /etc/systemd/user/graphical-session-pre.target.wants/xdg-desktop-portal.service + so that it will not cause a deadlock during GNOME login. + * d/p/Move-g_string_replace-backport-to-a-separate-translation-.patch, + d/p/rewrite-launchers-Provide-backported-g_string_replace.patch: + Add patches to fix compilation with GLib 2.66, to make backports more + straightforward. + + -- Simon McVittie Sun, 27 Mar 2022 13:05:55 +0100 + +xdg-desktop-portal (1.14.1-1) unstable; urgency=medium + + * New upstream release + * d/p/Revert-Make-x-d-p-start-on-session-start.patch: + Drop patch, the problem was fixed differently upstream + * Package the new xdg-desktop-portal-rewrite-launchers executable + + -- Simon McVittie Sat, 19 Mar 2022 14:08:29 +0000 + +xdg-desktop-portal (1.14.0-2) unstable; urgency=medium + + * d/p/Revert-Make-x-d-p-start-on-session-start.patch: + Revert starting xdg-desktop-portal during session start. + This causes a new GNOME login after a reboot to deadlock. + + -- Simon McVittie Fri, 18 Mar 2022 11:16:06 +0000 + +xdg-desktop-portal (1.14.0-1) unstable; urgency=medium + + * New upstream release + * Drop patches restoring the icon validator. + It has been reinstated upstream. + * Depend and build-depend on FUSE 3 + * d/control, d/rules: Build with libsystemd + + -- Simon McVittie Fri, 18 Mar 2022 09:17:39 +0000 + +xdg-desktop-portal (1.12.2-1) unstable; urgency=medium + + * New upstream release + + -- Simon McVittie Thu, 17 Mar 2022 23:25:45 +0000 + +xdg-desktop-portal (1.12.1-1) unstable; urgency=medium + + * New upstream release + * Drop patches that were applied upstream + * d/copyright: Update + + -- Simon McVittie Wed, 22 Dec 2021 15:46:31 +0000 + +xdg-desktop-portal (1.10.1-4) unstable; urgency=medium + + * Upload to unstable. + The libportal-based build-time test coverage passes on all release + architectures. + + -- Simon McVittie Tue, 16 Nov 2021 10:30:39 +0000 + +xdg-desktop-portal (1.10.1-3) experimental; urgency=medium + + * d/gbp.conf: Branch for experimental + * d/control, d/rules: Enable libportal now that it is available in testing + + -- Simon McVittie Mon, 15 Nov 2021 20:37:48 +0000 + +xdg-desktop-portal (1.10.1-2) unstable; urgency=medium + + * d/p/open-uri-Avoid-calling-into-o.fd.FileManager1-during-star.patch: + Add proposed patch to resolve deadlock with some file managers. + This avoids a long delay during startup of GtkApplication-based + implementations of the o.fd.FileManager1 interface, such as XFCE's + Thunar, in a non-GNOME and non-XFCE environment. (Closes: #994865) + * d/p/build-Fix-cross-compilation-by-adjusting-pkg-config-use.patch: + Add proposed patch to fix cross-compilation. + Thanks to Helmut Grohne (Closes: #985411) + + -- Simon McVittie Fri, 01 Oct 2021 23:29:32 +0100 + +xdg-desktop-portal (1.10.1-1) unstable; urgency=medium + + * New upstream release + * Standards-Version: 4.6.0 (no changes required) + * d/rules: Stop working around wrong permissions on test-document-fuse.py. + This was fixed upstream. + * Don't let debhelper 13 make installed-tests data executable + * Use debhelper compat level 13 + - No need to override dh_missing any more + + -- Simon McVittie Tue, 21 Sep 2021 17:21:24 +0100 + +xdg-desktop-portal (1.8.1-1) unstable; urgency=medium + + * New upstream release + - Fix a fd leak when opening URIs + - Fix directory support in filechooser + - Improve Snap support + - Fix a crash when setting wallpaper fails + * Drop fontconfig build-dependency, no longer needed + * Define _GNU_SOURCE when compiling the icon validator, + avoiding an implicit declaration of execvpe() + * Label the icon validator as coming from Flatpak 1.10.1 + (no actual changes, the code is identical to 1.8.1) + + -- Simon McVittie Fri, 19 Feb 2021 10:21:45 +0000 + +xdg-desktop-portal (1.8.0-3) unstable; urgency=medium + + * debian/rules: Build with pipewire support in ubuntu as well (LP: #1879580) + + -- Marco Trevisan (Treviño) Thu, 11 Feb 2021 19:08:03 +0100 + +xdg-desktop-portal (1.8.0-2) unstable; urgency=medium + + * Version the build-dependency on libflatpak-dev. + This is significant when backporting to buster. + * Preferentially build-depend on libgdk-pixbuf-2.0-dev. + We don't need the deprecated Xlib integration that is also pulled in + by the older libgdk-pixbuf2.0-dev package (see #974870). + * Standards-Version: 4.5.1 (no changes required) + + -- Simon McVittie Tue, 24 Nov 2020 12:01:38 +0000 + +xdg-desktop-portal (1.8.0-1) unstable; urgency=medium + + * New upstream release + - Drop most patches, applied upstream + + -- Simon McVittie Wed, 16 Sep 2020 10:31:24 +0100 + +xdg-desktop-portal (1.7.2-2) unstable; urgency=medium + + * d/rules: Don't explicitly use --as-needed linker option. + It's the default with bullseye toolchains. + * Release to unstable as part of the pipewire 0.3 transition + + -- Simon McVittie Thu, 10 Sep 2020 12:32:17 +0100 + +xdg-desktop-portal (1.7.2-1) experimental; urgency=medium + + * New upstream development release + * Drop patch skipping FUSE tests if necessary, + upstream made an equivalent change + * Update icon validator to the one from Flatpak 1.8.1 + * Build-Depend on libcap2-bin, for capsh, and add it to test dependencies + * tests: Look for capsh in /sbin + * Update patches. + This includes upstream 1.7.2-37-g089e72b plus merge request + , + to make the installed-tests work better. + * d/tests/gnome-desktop-testing: Set TEST_IN_CI. + Upstream uses this to extend test timeouts and skip flaky tests. + * Re-enable pipewire now that 0.3.x is in experimental + + -- Simon McVittie Mon, 07 Sep 2020 19:41:23 +0100 + +xdg-desktop-portal (1.7.1-1) experimental; urgency=medium + + * New upstream development release + + -- Simon McVittie Mon, 30 Mar 2020 10:19:22 +0100 + +xdg-desktop-portal (1.7.0-1) experimental; urgency=medium + + * Switch to experimental branch + - d/watch: Watch for development releases + - d/gbp.conf, d/control: Use debian/experimental packaging branch + - d/gbp.conf: Use upstream/latest branch + * New upstream development release + * Disable pipewire until 0.3.x is available (#954022) + * Drop patches that were applied upstream + * Set upstream metadata fields: Repository. + * Remove obsolete field Name from debian/upstream/metadata (already + present in machine-readable debian/copyright). + * Standards-Version: 4.5.0 (no changes required) + * d/p/tests-Divert-service-stdout-to-stderr.patch: Simplify + * tests: Depend on procps, for /bin/kill + * tests: Depend on python3 and python3-gi + * Add a patch to skip (more) FUSE tests on buildds + + -- Simon McVittie Wed, 25 Mar 2020 16:00:52 +0000 + +xdg-desktop-portal (1.6.0-1) unstable; urgency=medium + + * New upstream stable release + - d/watch: Only watch for stable releases + - d/gbp.conf: Use debian/master, upstream/1.6.x branches + * Mention #946913 in previous changelog entry + * Add "unsafe" pkg.libportal.enable build option to enable extra tests + while libportal is not yet API-stable + * d/p/Fix-the-build-on-old-glib.patch, + d/p/xml-Avoid-fancy-quotes.patch: + Add post-release fixes from upstream + * d/p/open-uri-Fix-criticals-if-no-default-handler-for-desired-.patch, + d/p/tests-Fix-race-condition-in-various-Lockdown-tests.patch, + d/p/tests-Skip-Location-tests-if-we-have-no-D-Bus-system-bus.patch, + d/p/tests-Install-test-backends.patch, + d/p/tests-When-installed-look-for-executables-in-libexecdir.patch, + d/p/tests-Install-test.portal-for-installed-tests.patch: + Fix some test failures when libportal is enabled + * d/p/tests-Divert-service-stdout-to-stderr.patch, + d/p/tests-Always-produce-verbose-output-from-subprocesses.patch: + Improve test diagnostics + * d/p/Replace-the-icon-validator-with-the-one-from-Flatpak-1.6..patch: + Relabel the icon validator as coming from Flatpak 1.6.0 + (it's the same as 1.4.2) + + -- Simon McVittie Tue, 24 Dec 2019 14:15:27 +0000 + +xdg-desktop-portal (1.5.4-1) experimental; urgency=medium + + * Branch for experimental + * New upstream development release + - d/patches: Rebase + - d/rules: Explicitly disable libportal-based tests (ITP: #946605) + - Only log one warning if unable to track which apps are "in the + background, for example x-d-p-gtk in a non-GNOME environment + (Closes: #946913) + + -- Simon McVittie Fri, 13 Dec 2019 23:23:14 +0000 + +xdg-desktop-portal (1.4.2-3) unstable; urgency=medium + + * Standards-Version: 4.4.1 (no changes required) + * Re-enable pipewire (screen recording) support now that v0.2.7 is + available + + -- Simon McVittie Sat, 02 Nov 2019 10:12:41 +0000 + +xdg-desktop-portal (1.4.2-2) unstable; urgency=medium + + * Release to unstable + * d/salsa-ci.yml: Request standard CI on salsa.debian.org + * d/p/test-doc-portal-Check-for-FUSE-support-more-thoroughly.patch: + Skip tests when FUSE is non-functional, even if we are root + * Standards-Version: 4.4.0 (no changes required) + * Use debhelper-compat 12 + - Remove redundant --libexecdir=/usr/libexec, which is the default + at this compat level + * d/p/Replace-the-icon-validator-with-the-one-from-Flatpak-1.4..patch: + Update icon validator to the one from Flatpak 1.4.2 + * Mark autopkgtest as flaky due to #931726 + + -- Simon McVittie Tue, 16 Jul 2019 09:19:47 +0100 + +xdg-desktop-portal (1.4.2-1) experimental; urgency=medium + + * New upstream release + * Disable pipewire (screencasting) until 0.2.6 is available in Debian + * Build-depend on libjson-glib-dev + * d/copyright: Update + + -- Simon McVittie Mon, 10 Jun 2019 09:37:22 +0100 + +xdg-desktop-portal (1.2.0-1) unstable; urgency=medium + + [ Simon McVittie ] + * New upstream stable release + * d/watch: Only watch for stable releases + * d/p/Revert-Stop-building-the-icon-validator.patch, + d/p/validate-icon-Add-a-define-for-bwrap.patch, + d/p/Replace-the-icon-validator-with-the-one-from-Flatpak-git-.patch: + Build a copy of the icon validator from Flatpak git master. + We don't want to rely on a version of Flatpak that isn't + stable yet. + - d/p/notification-Handle-non-existing-directories.patch, + d/p/notification-bind-mount-etc-ld.so.cache-to-the-sandbox.patch: + Remove, no longer necessary + * d/upstream/metadata: Add DEP-12 metadata + * Release to unstable + + [ Jeremy Bicha ] + * Don't enable remote desktop support on Ubuntu. See LP bug 1802533 + + -- Simon McVittie Sat, 26 Jan 2019 18:31:50 +0000 + +xdg-desktop-portal (1.1.1-1) experimental; urgency=medium + + * New upstream development release + - Drop patches, applied upstream + - Build-Depend on libgdk-pixbuf2.0-dev, for icon validation + - Package new xdg-desktop-portal-validate-icon executable + - Depend on bubblewrap, for sandboxed icon validation + * Enable Location portal (geoclue-2.0) + * Standards-Version: 4.3.0 (no changes required) + * d/control: Set Vcs-Git branch for experimental + * d/p/notification-Handle-non-existing-directories.patch, + d/p/notification-bind-mount-etc-ld.so.cache-to-the-sandbox.patch: + Make bwrap invocation more reliable, and in particular make it + work on purely 32-bit systems with no /lib64 (patches taken from + gnome-desktop, thanks to Iain Lane and Mart Raudsepp) + + -- Simon McVittie Thu, 17 Jan 2019 09:57:59 +0000 + +xdg-desktop-portal (1.1.0-1) experimental; urgency=medium + + * New upstream development release + - d/copyright: Update + - d/control: Build-depend on fontconfig, for fontconfig cache + invalidation in Settings portal + - d/control: Build-depend on flatpak, for permission-store test + * Don't build with Location portal yet, because geoclue-2.0 is + currently too old + * Drop patch, applied upstream + * d/gbp.conf: Use debian/experimental branch + * d/patches: Skip another build-time test when FUSE is not supported + * d/patches: Don't show a critical warning when a document permissions + entry is removed + * Enable Pipewire support, for screencasting + + -- Simon McVittie Mon, 19 Nov 2018 18:47:57 +0000 + +xdg-desktop-portal (1.0.3-1) unstable; urgency=medium + + * New upstream release + * d/p/desktop-portal-don-t-attempt-to-export-NULL-GDBusInterfac.patch: + Add patch from upstream to guard against NULL interface objects + (previously applied in Ubuntu, LP: #1691649) + + -- Simon McVittie Sat, 13 Oct 2018 11:33:35 +0100 + +xdg-desktop-portal (1.0.2-1) unstable; urgency=medium + + * New upstream release + * d/p/network-monitor-Build-variants-for-GetStatus-correctly.patch: + Drop, superseded by upstream changes + + -- Simon McVittie Mon, 10 Sep 2018 10:37:46 +0100 + +xdg-desktop-portal (1.0.1-1) unstable; urgency=medium + + * New upstream release + * d/p/network-monitor-Build-variants-for-GetStatus-correctly.patch: + Add patch to make NetworkMonitor.GetStatus() work as intended + * Standards-Version: 4.2.1 (no changes required) + + -- Simon McVittie Fri, 31 Aug 2018 08:55:19 +0100 + +xdg-desktop-portal (1.0-1) unstable; urgency=medium + + * New upstream release + * Install upstream NEWS + * Standards-Version: 4.2.0 (no further changes) + + -- Simon McVittie Thu, 23 Aug 2018 09:53:13 +0100 + +xdg-desktop-portal (0.99-1) unstable; urgency=medium + + * New upstream release + * Standards-Version: 4.1.5 + * Install to /usr/libexec + * Build in a UTF-8 locale, to avoid FTBFS when gdbus-codegen reads + UTF-8 + + -- Simon McVittie Sat, 28 Jul 2018 12:28:48 +0100 + +xdg-desktop-portal (0.11-2) unstable; urgency=medium + + [ Jeremy Bicha ] + * Update package description to mention Snap + * Use github page as homepage + + -- Simon McVittie Sun, 24 Jun 2018 22:02:48 +0100 + +xdg-desktop-portal (0.11-1) unstable; urgency=medium + + * New upstream release + - Drop patch, applied upstream + * Standards-Version: 4.1.4 (no changes) + + -- Simon McVittie Wed, 25 Apr 2018 16:29:22 +0100 + +xdg-desktop-portal (0.10-4) unstable; urgency=medium + + * Merge from experimental, with autopkgtests included + * d/p/build-Run-install-test-data-hook-as-intended.patch: + Mark patch as applied upstream + + -- Simon McVittie Fri, 16 Feb 2018 14:50:44 +0000 + +xdg-desktop-portal (0.10-3) experimental; urgency=medium + + * Reinstate xdg-desktop-portal-tests package and autopkgtests + * d/p/build-Run-install-test-data-hook-as-intended.patch: + Run the tests' install hook to set up enough symlinks that they + will pass + * Upload to experimental to wait for NEW processing + + -- Simon McVittie Thu, 15 Feb 2018 00:30:01 +0000 + +xdg-desktop-portal (0.10-2) unstable; urgency=medium + + * Really remove xdg-desktop-portal-tests stanza from d/control + + -- Simon McVittie Thu, 15 Feb 2018 00:01:42 +0000 + +xdg-desktop-portal (0.10-1) unstable; urgency=medium + + * New upstream release + * Add Breaks/Replaces for flatpak (<< 0.10.4-1~) since this package + has taken over the permission store and documents portal + * Disable Pipewire support for now (ITP: #874089) + * Add new build-dependencies for FUSE + * Install the permission store and documents portal + * d/copyright: Update + * Drop flatpak build-dependency, no longer needed + * Remove patches, applied upstream + * Depend on FUSE + * Build installed-tests, but don't install them for now to avoid the + NEW queue + * Use dh_missing --fail-missing instead of dh_install --fail-missing + + -- Simon McVittie Wed, 14 Feb 2018 17:44:15 +0000 + +xdg-desktop-portal (0.9-2) unstable; urgency=medium + + * Standards-Version: 4.1.3 (no changes) + * Change Vcs-* to point to salsa.debian.org + * d/p/0.10/: Update to upstream git commit 0.9-9-g422ecf3 for various + bug fixes + + -- Simon McVittie Thu, 18 Jan 2018 08:46:29 +0000 + +xdg-desktop-portal (0.9-1) unstable; urgency=medium + + * New upstream release + - Drop all patches, applied upstream + + -- Simon McVittie Fri, 24 Nov 2017 10:38:16 +0000 + +xdg-desktop-portal (0.8-3) unstable; urgency=medium + + * Set Rules-Requires-Root to no + * Use https Format URL in d/copyright + * Standards-Version: 4.1.1 (no further changes) + + -- Simon McVittie Sat, 11 Nov 2017 13:43:41 +0000 + +xdg-desktop-portal (0.8-2) unstable; urgency=medium + + * Mark patches as applied upstream for 0.9 + * Upload to unstable + + -- Simon McVittie Sat, 10 Jun 2017 10:12:20 +0100 + +xdg-desktop-portal (0.8-1) experimental; urgency=medium + + * New upstream release + * Add patches to make the development files completely + architecture-independent (Closes: #864451) + * Make xdg-desktop-portal-dev Architecture: all + + -- Simon McVittie Fri, 09 Jun 2017 08:04:12 +0100 + +xdg-desktop-portal (0.6-1) experimental; urgency=medium + + * New upstream release + - Run as a systemd user service if dbus-user-session is installed + - Add email portal + * Move to debhelper compat level 10 + * Omit unimplemented configure option --enable-installed-tests + * Do not explicitly disable quiet Automake output: dh now does this + by default + * Move API documentation from main package to -dev package + * Register API documentation with doc-base + + -- Simon McVittie Mon, 03 Apr 2017 15:28:02 +0100 + +xdg-desktop-portal (0.5-1) unstable; urgency=medium + + * New upstream release + + -- Simon McVittie Sat, 21 Jan 2017 16:28:36 +0000 + +xdg-desktop-portal (0.4-1) unstable; urgency=medium + + * New upstream release + - Drop cherry-picked patch for flatpak 0.6.10 container detection, + now included in the upstream release + + -- Simon McVittie Mon, 05 Dec 2016 10:51:02 +0000 + +xdg-desktop-portal (0.3-1) unstable; urgency=medium + + * Initial release. (Closes: #831689) + + -- Simon McVittie Wed, 21 Sep 2016 22:14:13 +0100 diff --git a/control b/control new file mode 100644 index 0000000..b6962f1 --- /dev/null +++ b/control @@ -0,0 +1,101 @@ +Source: xdg-desktop-portal +Section: admin +Priority: optional +Maintainer: Utopia Maintenance Team +Uploaders: + Simon McVittie , +Build-Depends: + dbus-daemon, + debhelper-compat (= 13), + geoclue-2.0 , + fuse3 , + libcap2-bin , + libgdk-pixbuf-2.0-dev, + libgeoclue-2-dev, + libflatpak-dev, + libfuse3-dev, + libglib2.0-dev, + libjson-glib-dev, + libpipewire-0.3-dev, + libportal-dev (>= 0.3), + libsystemd-dev, + meson, + pipewire , + procps , + python3 , + python3-dbusmock , + python3-gi , + python3-pytest , + xmlto, +Rules-Requires-Root: no +Standards-Version: 4.6.2 +Homepage: https://github.com/flatpak/xdg-desktop-portal +Vcs-Git: https://salsa.debian.org/debian/xdg-desktop-portal.git +Vcs-Browser: https://salsa.debian.org/debian/xdg-desktop-portal + +Package: xdg-desktop-portal +Architecture: linux-any +Multi-Arch: foreign +Depends: + bubblewrap, + default-dbus-session-bus | dbus-session-bus, + fuse3, + ${misc:Depends}, + ${shlibs:Depends}, +Description: desktop integration portal for Flatpak and Snap + xdg-desktop-portal provides a portal frontend service for Flatpak, Snap, + and possibly other desktop containment/sandboxing frameworks. This service + is made available to the sandboxed application, and provides mediated + D-Bus interfaces for file access, URI opening, printing and similar + desktop integration features. + . + The implementation of these interfaces is expected to require + user confirmation before responding to the sandboxed application's + requests. For example, when the sandboxed application ask to open a file, + the portal implementation will open an "Open" dialog outside the sandbox, + and will only make the selected file available to the sandboxed app if + that dialog is confirmed. + . + xdg-desktop-portal is designed to be desktop-agnostic, and uses a + desktop-environment-specific GUI backend such as xdg-desktop-portal-gtk + to provide its functionality. + +Package: xdg-desktop-portal-dev +Architecture: all +Multi-Arch: foreign +Depends: + ${misc:Depends}, + ${shlibs:Depends}, +Description: desktop integration portal - development files + xdg-desktop-portal provides a portal frontend service for Flatpak, Snap, + and possibly other desktop containment/sandboxing frameworks. This service + is made available to the sandboxed application, and provides mediated + D-Bus interfaces for file access, URI opening, printing and similar + desktop integration features. See the xdg-desktop-portal package's + description for more details. + . + This package contains development files for backends such as + xdg-desktop-portal-gtk. + +Package: xdg-desktop-portal-tests +Architecture: any +Depends: + dbus-daemon, + geoclue-2.0, + libcap2-bin, + pipewire, + procps, + python3, + python3-gi, + xdg-desktop-portal, + ${misc:Depends}, + ${shlibs:Depends}, +Description: desktop integration portal - automated tests + xdg-desktop-portal provides a portal frontend service for Flatpak, Snap, + and possibly other desktop containment/sandboxing frameworks. This service + is made available to the sandboxed application, and provides mediated + D-Bus interfaces for file access, URI opening, printing and similar + desktop integration features. See the xdg-desktop-portal package's + description for more details. + . + This package contains automated tests. diff --git a/copyright b/copyright new file mode 100644 index 0000000..3da0e9f --- /dev/null +++ b/copyright @@ -0,0 +1,67 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: xdg-desktop-portal +Source: https://github.com/flatpak/xdg-desktop-portal/releases + +Files: + * +Copyright: + © 2010 Codethink Limited + © 2013-2019 Red Hat, Inc + © 2016 Free Software Foundation, Inc. + © 2016 Piotr Drag + © 2016 Aviary.pl + © 2017 Jan Alexander Steffens + © 2018-2021 Igalia S.L. + © 2022 Aleix Pol Gonzalez + © 2022 Endless OS Foundation, LLC + © 2022 Georges Basile Stavracas Neto +License: LGPL-2+ + +Files: + src/flatpak-instance.* +Copyright: + © 2018 Red Hat, Inc +License: LGPL-2.1+ + +Files: + debian/* +Copyright: + © 2016-2018 Simon McVittie + © 2016-2018 Collabora Ltd. +License: LGPL-2+ + +License: LGPL-2+ + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + . + This library 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 + Lesser General Public License for more details. + . + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +Comment: + On Debian systems, the full text of the GNU Lesser General Public License + version 2.1 can be found in the file '/usr/share/common-licenses/LGPL-2.1'. + +License: LGPL-2.1+ + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + . + This library 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 + Lesser General Public License for more details. + . + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +Comment: + On Debian systems, the full text of the GNU Lesser General Public License + version 2.1 can be found in the file '/usr/share/common-licenses/LGPL-2.1'. diff --git a/gbp.conf b/gbp.conf new file mode 100644 index 0000000..f56457d --- /dev/null +++ b/gbp.conf @@ -0,0 +1,7 @@ +[DEFAULT] +pristine-tar = True +compression = xz +debian-branch = debian/master +upstream-branch = upstream/latest +patch-numbers = False +upstream-vcs-tag = %(version)s diff --git a/not-installed b/not-installed new file mode 100644 index 0000000..7b5d0e4 --- /dev/null +++ b/not-installed @@ -0,0 +1 @@ +usr/share/doc/xdg-desktop-portal/redirect.html diff --git a/patches/open-uri-Fix-a-presumably-copy-paste-typo.patch b/patches/open-uri-Fix-a-presumably-copy-paste-typo.patch new file mode 100644 index 0000000..8a3d715 --- /dev/null +++ b/patches/open-uri-Fix-a-presumably-copy-paste-typo.patch @@ -0,0 +1,24 @@ +From: Ilya Fedin +Date: Mon, 7 Aug 2023 12:52:32 +0400 +Subject: open-uri: Fix a (presumably) copy-paste typo + +(cherry picked from commit 4434b20487e734b3b42ed0b1d1035a323a37c945) + +Origin: future 1.17.1 +--- + src/open-uri.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/open-uri.c b/src/open-uri.c +index 9cddc6e..880399c 100644 +--- a/src/open-uri.c ++++ b/src/open-uri.c +@@ -1074,7 +1074,7 @@ handle_open_directory (XdpDbusOpenURI *object, + g_object_set_data_full (G_OBJECT (request), "activation-token", g_strdup (activation_token), g_free); + + request_export (request, g_dbus_method_invocation_get_connection (invocation)); +- xdp_dbus_open_uri_complete_open_file (object, invocation, NULL, request->id); ++ xdp_dbus_open_uri_complete_open_directory (object, invocation, NULL, request->id); + + task = g_task_new (object, NULL, NULL, NULL); + g_task_set_task_data (task, g_object_ref (request), g_object_unref); diff --git a/patches/open-uri-Pass-activation-token-to-the-FileManager1-interf.patch b/patches/open-uri-Pass-activation-token-to-the-FileManager1-interf.patch new file mode 100644 index 0000000..e937214 --- /dev/null +++ b/patches/open-uri-Pass-activation-token-to-the-FileManager1-interf.patch @@ -0,0 +1,24 @@ +From: Ilya Fedin +Date: Mon, 7 Aug 2023 12:50:54 +0400 +Subject: open-uri: Pass activation token to the FileManager1 interface + +(cherry picked from commit 69e2faef94f12734706c942ae99248a122fe2f49) + +Origin: future 1.17.1 +--- + src/open-uri.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/open-uri.c b/src/open-uri.c +index ef9cf49..9cddc6e 100644 +--- a/src/open-uri.c ++++ b/src/open-uri.c +@@ -741,7 +741,7 @@ handle_open_in_thread_func (GTask *task, + FILE_MANAGER_DBUS_PATH, + FILE_MANAGER_DBUS_IFACE, + FILE_MANAGER_SHOW_ITEMS, +- g_variant_new ("(ass)", uris_builder, ""), ++ g_variant_new ("(ass)", uris_builder, activation_token), + NULL, /* ignore returned type */ + G_DBUS_CALL_FLAGS_NONE, + -1, diff --git a/patches/series b/patches/series new file mode 100644 index 0000000..86a017b --- /dev/null +++ b/patches/series @@ -0,0 +1,3 @@ +open-uri-Pass-activation-token-to-the-FileManager1-interf.patch +open-uri-Fix-a-presumably-copy-paste-typo.patch +settings-Fix-values-from-read-being-variants-inside-varia.patch diff --git a/patches/settings-Fix-values-from-read-being-variants-inside-varia.patch b/patches/settings-Fix-values-from-read-being-variants-inside-varia.patch new file mode 100644 index 0000000..6bd44eb --- /dev/null +++ b/patches/settings-Fix-values-from-read-being-variants-inside-varia.patch @@ -0,0 +1,124 @@ +From: Robert Ancell +Date: Thu, 1 Sep 2022 14:42:02 +1200 +Subject: settings: Fix values from read being variants inside variants. + +The backend returns a variant, and this was mistakenly being put inside another +variant when returned to the original caller. Add a new ReadOne method that +doesn't have this issue. + +Fixes https://github.com/flatpak/xdg-desktop-portal/issues/789 + +(cherry picked from commit c28dbdd990d7abd25a22128c96c6a3c521d2abe0) + +Origin: future 1.17.1 +--- + data/org.freedesktop.portal.Settings.xml | 23 ++++++++++++++++++- + src/settings.c | 39 +++++++++++++++++++++++++++++++- + 2 files changed, 60 insertions(+), 2 deletions(-) + +diff --git a/data/org.freedesktop.portal.Settings.xml b/data/org.freedesktop.portal.Settings.xml +index 669997a..7d16cc7 100644 +--- a/data/org.freedesktop.portal.Settings.xml ++++ b/data/org.freedesktop.portal.Settings.xml +@@ -49,7 +49,7 @@ + implementation details that are undocumented. If you are a + toolkit and want to use this please open an issue. + +- This documentation describes version 1 of this interface. ++ This documentation describes version 2 of this interface. + --> + + +@@ -73,8 +73,29 @@ + @value: The value @key is set to. + + Reads a single value. Returns an error on any unknown namespace or key. ++ ++ Deprecated, use ReadOne instead. The value argument was intended to have ++ the value inside one layer of variant as it is in ReadOne, for example ++ `<string "hello">` in GVariant text notation; but it is actually ++ returned inside two layers of variant, for example ++ `<<string "hello">>`. + --> + ++ ++ ++ ++ ++ ++ ++ ++ + + + +diff --git a/src/settings.c b/src/settings.c +index 4a60cdd..4890c03 100644 +--- a/src/settings.c ++++ b/src/settings.c +@@ -129,6 +129,42 @@ settings_handle_read (XdpDbusSettings *object, + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + ++static gboolean ++settings_handle_read_one (XdpDbusSettings *object, ++ GDBusMethodInvocation *invocation, ++ const char *arg_namespace, ++ const char *arg_key) ++{ ++ int i; ++ ++ g_debug ("ReadOne %s %s", arg_namespace, arg_key); ++ ++ for (i = 0; i < n_impls; i++) ++ { ++ g_autoptr(GError) error = NULL; ++ g_autoptr(GVariant) impl_value = NULL; ++ ++ if (!xdp_dbus_impl_settings_call_read_sync (impls[i], arg_namespace, ++ arg_key, &impl_value, NULL, &error)) ++ { ++ /* A key not being found is expected, continue to our implementation */ ++ g_debug ("Failed to Read() from Settings implementation: %s", error->message); ++ } ++ else ++ { ++ g_dbus_method_invocation_return_value (invocation, g_variant_new_tuple (&impl_value, 1)); ++ return G_DBUS_METHOD_INVOCATION_HANDLED; ++ } ++ } ++ ++ g_debug ("Attempted to read unknown namespace/key pair: %s %s", arg_namespace, arg_key); ++ g_dbus_method_invocation_return_error_literal (invocation, XDG_DESKTOP_PORTAL_ERROR, ++ XDG_DESKTOP_PORTAL_ERROR_NOT_FOUND, ++ _("Requested setting not found")); ++ ++ return G_DBUS_METHOD_INVOCATION_HANDLED; ++} ++ + static void + on_impl_settings_changed (XdpDbusImplSettings *impl, + const char *arg_namespace, +@@ -145,13 +181,14 @@ static void + settings_iface_init (XdpDbusSettingsIface *iface) + { + iface->handle_read = settings_handle_read; ++ iface->handle_read_one = settings_handle_read_one; + iface->handle_read_all = settings_handle_read_all; + } + + static void + settings_init (Settings *settings) + { +- xdp_dbus_settings_set_version (XDP_DBUS_SETTINGS (settings), 1); ++ xdp_dbus_settings_set_version (XDP_DBUS_SETTINGS (settings), 2); + } + + static void diff --git a/rules b/rules new file mode 100755 index 0000000..964d769 --- /dev/null +++ b/rules @@ -0,0 +1,35 @@ +#!/usr/bin/make -f + +export DEB_BUILD_MAINT_OPTIONS = hardening=+all +export LC_ALL=C.UTF-8 + +include /usr/share/dpkg/default.mk + +built_binaries := $(shell dh_listpackages) + +CONFFLAGS := + +%: + dh $@ --buildsystem=meson + +override_dh_auto_configure: + dh_auto_configure -- \ + -Dauto_features=enabled \ + -Dinstalled-tests=true \ + $(CONFFLAGS) + $(NULL) + +override_dh_auto_test: +ifeq ($(filter nocheck,$(DEB_BUILD_OPTIONS)),) + G_MESSAGES_DEBUG=all \ + TEST_IN_CI=1 \ + dh_auto_test --no-parallel +endif + +# debhelper >= 13.4 makes all of /usr/libexec executable, which is not +# quite right for installed-tests +override_dh_fixperms: + dh_fixperms -Xusr/libexec/installed-tests +ifneq ($(filter %-tests,$(built_binaries)),) + chmod --recursive --changes a+rX,u+w,og-w debian/*-tests/usr/libexec/installed-tests +endif diff --git a/salsa-ci.yml b/salsa-ci.yml new file mode 100644 index 0000000..0c22dc4 --- /dev/null +++ b/salsa-ci.yml @@ -0,0 +1,3 @@ +include: + - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/salsa-ci.yml + - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/pipeline-jobs.yml diff --git a/source/format b/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/tests/control b/tests/control new file mode 100644 index 0000000..8094e12 --- /dev/null +++ b/tests/control @@ -0,0 +1,5 @@ +Tests: gnome-desktop-testing +Restrictions: flaky, isolation-machine +Depends: + gnome-desktop-testing, + xdg-desktop-portal-tests, diff --git a/tests/gnome-desktop-testing b/tests/gnome-desktop-testing new file mode 100755 index 0000000..27ca6f9 --- /dev/null +++ b/tests/gnome-desktop-testing @@ -0,0 +1,12 @@ +#!/bin/sh + +set -e +exec 2>&1 + +# capsh is in /sbin +export PATH="$PATH:/sbin:/usr/sbin" + +# Some tests are skipped or have longer timeouts in CI +export TEST_IN_CI=1 + +exec gnome-desktop-testing-runner xdg-desktop-portal diff --git a/upstream/metadata b/upstream/metadata new file mode 100644 index 0000000..24ff072 --- /dev/null +++ b/upstream/metadata @@ -0,0 +1,6 @@ +--- +Repository: https://github.com/flatpak/xdg-desktop-portal.git +Repository-Browse: https://github.com/flatpak/xdg-desktop-portal +Bug-Database: https://github.com/flatpak/xdg-desktop-portal/issues +Bug-Submit: https://github.com/flatpak/xdg-desktop-portal/issues/new +Security-Contact: https://github.com/flatpak/xdg-desktop-portal/tree/HEAD/SECURITY.md diff --git a/watch b/watch new file mode 100644 index 0000000..28c5196 --- /dev/null +++ b/watch @@ -0,0 +1,9 @@ +version=4 +# Upstream releases official Autotools 'make dist' tarballs, so we use +# those in preference to git tags +opts="\ + compression=xz, \ + dversionmangle=s/\+(?:git)?[0-9]*(?:\+g[0-9a-f]*)//, \ + downloadurlmangle=s#/tag/#/download/#;s#(v?@ANY_VERSION@)$#$1/@PACKAGE@-$2.tar.xz#, \ + filenamemangle=s#v?@ANY_VERSION@#@PACKAGE@-$1.tar.xz#" \ +https://github.com/flatpak/@PACKAGE@/tags .*/releases/tag/v?@ANY_VERSION@ diff --git a/xdg-desktop-portal-dev.doc-base b/xdg-desktop-portal-dev.doc-base new file mode 100644 index 0000000..bcbb95b --- /dev/null +++ b/xdg-desktop-portal-dev.doc-base @@ -0,0 +1,8 @@ +Document: xdg-desktop-portal-dev +Title: Portal API Reference +Author: Matthias Clasen +Section: Programming + +Format: HTML +Index: /usr/share/doc/xdg-desktop-portal/portal-docs.html +Files: /usr/share/doc/xdg-desktop-portal/portal-docs.html /usr/share/doc/xdg-desktop-portal/docbook.css diff --git a/xdg-desktop-portal-dev.install b/xdg-desktop-portal-dev.install new file mode 100644 index 0000000..62e5ec5 --- /dev/null +++ b/xdg-desktop-portal-dev.install @@ -0,0 +1,4 @@ +usr/share/dbus-1/interfaces +usr/share/doc/xdg-desktop-portal/docbook.css +usr/share/doc/xdg-desktop-portal/portal-docs.html +usr/share/pkgconfig/xdg-desktop-portal.pc diff --git a/xdg-desktop-portal-tests.install b/xdg-desktop-portal-tests.install new file mode 100644 index 0000000..6d3b245 --- /dev/null +++ b/xdg-desktop-portal-tests.install @@ -0,0 +1,2 @@ +usr/libexec/installed-tests/xdg-desktop-portal +usr/share/installed-tests/xdg-desktop-portal diff --git a/xdg-desktop-portal.docs b/xdg-desktop-portal.docs new file mode 100644 index 0000000..edc0071 --- /dev/null +++ b/xdg-desktop-portal.docs @@ -0,0 +1 @@ +NEWS diff --git a/xdg-desktop-portal.install b/xdg-desktop-portal.install new file mode 100644 index 0000000..8d8fa5d --- /dev/null +++ b/xdg-desktop-portal.install @@ -0,0 +1,8 @@ +usr/lib/systemd/user +usr/libexec/xdg-desktop-portal +usr/libexec/xdg-desktop-portal-rewrite-launchers +usr/libexec/xdg-desktop-portal-validate-icon +usr/libexec/xdg-document-portal +usr/libexec/xdg-permission-store +usr/share/dbus-1/services +usr/share/locale/*/*/xdg-desktop-portal.mo diff --git a/xdg-desktop-portal.lintian-overrides b/xdg-desktop-portal.lintian-overrides new file mode 100644 index 0000000..b6add39 --- /dev/null +++ b/xdg-desktop-portal.lintian-overrides @@ -0,0 +1,2 @@ +# https://bugs.debian.org/1031037 +no-manual-page [usr/libexec/*] diff --git a/xdg-desktop-portal.postinst b/xdg-desktop-portal.postinst new file mode 100644 index 0000000..01d0b6a --- /dev/null +++ b/xdg-desktop-portal.postinst @@ -0,0 +1,11 @@ +#!/bin/sh + +set -e + +if [ -n "$2" ] && dpkg --compare-versions "$2" ge 1.14.0 && dpkg --compare-versions "$2" lt 1.14.1-1~bpo11+1; then + deb-systemd-helper --user disable xdg-desktop-portal.service +fi + +#DEBHELPER# + +# vim:set sw=4 sts=4 et: -- cgit v1.2.3 From e1bf65dd0b83f3c42cee6c0032355a0f225a2881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20B=C3=ADcha?= Date: Mon, 21 Aug 2023 21:33:43 +0100 Subject: Import xdg-desktop-portal_1.17.0.orig.tar.xz [dgit import orig xdg-desktop-portal_1.17.0.orig.tar.xz] --- .github/workflows/check.yml | 130 + .github/workflows/docs.yml | 71 + CONTRIBUTING.md | 58 + COPYING | 502 +++ NEWS | 500 +++ README.md | 89 + RELEASE_HOWTO.md | 19 + SECURITY.md | 16 + data/meson.build | 74 + data/org.freedesktop.background.Monitor.xml | 64 + data/org.freedesktop.impl.portal.Access.xml | 95 + data/org.freedesktop.impl.portal.Account.xml | 91 + data/org.freedesktop.impl.portal.AppChooser.xml | 123 + data/org.freedesktop.impl.portal.Background.xml | 114 + data/org.freedesktop.impl.portal.Clipboard.xml | 180 + ...org.freedesktop.impl.portal.DynamicLauncher.xml | 148 + data/org.freedesktop.impl.portal.Email.xml | 97 + data/org.freedesktop.impl.portal.FileChooser.xml | 346 ++ ...org.freedesktop.impl.portal.GlobalShortcuts.xml | 176 + data/org.freedesktop.impl.portal.Inhibit.xml | 141 + data/org.freedesktop.impl.portal.InputCapture.xml | 436 +++ data/org.freedesktop.impl.portal.Lockdown.xml | 42 + data/org.freedesktop.impl.portal.Notification.xml | 81 + ...org.freedesktop.impl.portal.PermissionStore.xml | 199 ++ data/org.freedesktop.impl.portal.Print.xml | 151 + data/org.freedesktop.impl.portal.RemoteDesktop.xml | 467 +++ data/org.freedesktop.impl.portal.Request.xml | 47 + data/org.freedesktop.impl.portal.ScreenCast.xml | 278 ++ data/org.freedesktop.impl.portal.Screenshot.xml | 120 + data/org.freedesktop.impl.portal.Secret.xml | 66 + data/org.freedesktop.impl.portal.Session.xml | 53 + data/org.freedesktop.impl.portal.Settings.xml | 100 + data/org.freedesktop.impl.portal.Wallpaper.xml | 67 + data/org.freedesktop.portal.Account.xml | 94 + data/org.freedesktop.portal.Background.xml | 133 + data/org.freedesktop.portal.Camera.xml | 84 + data/org.freedesktop.portal.Clipboard.xml | 182 + data/org.freedesktop.portal.Device.xml | 63 + data/org.freedesktop.portal.Documents.xml | 279 ++ data/org.freedesktop.portal.DynamicLauncher.xml | 329 ++ data/org.freedesktop.portal.Email.xml | 124 + data/org.freedesktop.portal.FileChooser.xml | 430 +++ data/org.freedesktop.portal.FileTransfer.xml | 158 + data/org.freedesktop.portal.GameMode.xml | 220 ++ data/org.freedesktop.portal.GlobalShortcuts.xml | 268 ++ data/org.freedesktop.portal.Inhibit.xml | 186 + data/org.freedesktop.portal.InputCapture.xml | 587 ++++ data/org.freedesktop.portal.Location.xml | 168 + data/org.freedesktop.portal.MemoryMonitor.xml | 50 + data/org.freedesktop.portal.NetworkMonitor.xml | 139 + data/org.freedesktop.portal.Notification.xml | 172 + data/org.freedesktop.portal.OpenURI.xml | 191 ++ .../org.freedesktop.portal.PowerProfileMonitor.xml | 43 + data/org.freedesktop.portal.Print.xml | 396 +++ data/org.freedesktop.portal.ProxyResolver.xml | 49 + data/org.freedesktop.portal.Realtime.xml | 65 + data/org.freedesktop.portal.RemoteDesktop.xml | 489 +++ data/org.freedesktop.portal.Request.xml | 86 + data/org.freedesktop.portal.ScreenCast.xml | 324 ++ data/org.freedesktop.portal.Screenshot.xml | 117 + data/org.freedesktop.portal.Secret.xml | 88 + data/org.freedesktop.portal.Session.xml | 75 + data/org.freedesktop.portal.Settings.xml | 99 + data/org.freedesktop.portal.Trash.xml | 48 + data/org.freedesktop.portal.Wallpaper.xml | 104 + doc/docbook.css | 95 + doc/meson.build | 66 + doc/portal-docs.xml.in | 188 ++ doc/portals-conf.rst | 87 + doc/redirect.html | 19 + doc/xmlto-config.xsl | 13 + document-portal/document-enums.h | 23 + document-portal/document-portal-fuse.c | 3554 ++++++++++++++++++++ document-portal/document-portal-fuse.h | 25 + document-portal/document-portal.c | 1701 ++++++++++ document-portal/document-portal.h | 49 + document-portal/document-store.c | 158 + document-portal/document-store.h | 38 + document-portal/file-transfer.c | 580 ++++ document-portal/file-transfer.h | 27 + document-portal/gvdb/README | 7 + document-portal/gvdb/gvdb-builder.c | 539 +++ document-portal/gvdb/gvdb-builder.h | 60 + document-portal/gvdb/gvdb-format.h | 85 + document-portal/gvdb/gvdb-reader.c | 718 ++++ document-portal/gvdb/gvdb-reader.h | 63 + document-portal/gvdb/gvdb.doap | 32 + document-portal/meson.build | 98 + ...edesktop.impl.portal.PermissionStore.service.in | 4 + .../org.freedesktop.portal.Documents.service.in | 5 + document-portal/permission-db.c | 1286 +++++++ document-portal/permission-db.h | 104 + document-portal/permission-store.c | 140 + document-portal/xdg-document-portal.service.in | 9 + document-portal/xdg-permission-store.c | 541 +++ document-portal/xdg-permission-store.h | 23 + document-portal/xdg-permission-store.service.in | 9 + meson.build | 193 ++ meson_options.txt | 44 + po/LINGUAS | 31 + po/POTFILES.in | 7 + po/be.po | 185 + po/ca.po | 194 ++ po/cs.po | 188 ++ po/da.po | 189 ++ po/de.po | 201 ++ po/en_GB.po | 191 ++ po/es.po | 194 ++ po/fr.po | 199 ++ po/gl.po | 194 ++ po/he.po | 183 + po/hi.po | 183 + po/hr.po | 190 ++ po/hu.po | 193 ++ po/id.po | 186 + po/it.po | 197 ++ po/ka.po | 193 ++ po/lt.po | 195 ++ po/meson.build | 1 + po/nl.po | 201 ++ po/oc.po | 193 ++ po/pl.po | 193 ++ po/pt.po | 195 ++ po/pt_BR.po | 198 ++ po/ro.po | 195 ++ po/ru.po | 194 ++ po/sk.po | 190 ++ po/sr.po | 200 ++ po/sv.po | 191 ++ po/tr.po | 194 ++ po/uk.po | 196 ++ po/zh_CN.po | 186 + po/zh_TW.po | 182 + src/account.c | 269 ++ src/account.h | 26 + src/background-monitor.c | 165 + src/background-monitor.h | 38 + src/background.c | 1206 +++++++ src/background.h | 28 + src/call.c | 47 + src/call.h | 33 + src/camera.c | 455 +++ src/camera.h | 24 + src/clipboard.c | 539 +++ src/clipboard.h | 26 + src/device.c | 365 ++ src/device.h | 37 + src/documents.c | 246 ++ src/documents.h | 43 + src/dynamic-launcher.c | 1047 ++++++ src/dynamic-launcher.h | 31 + src/email.c | 336 ++ src/email.h | 26 + src/file-chooser.c | 911 +++++ src/file-chooser.h | 27 + src/flatpak-instance.c | 523 +++ src/flatpak-instance.h | 64 + src/gamemode.c | 607 ++++ src/gamemode.h | 25 + src/glib-backports.c | 72 + src/glib-backports.h | 42 + src/global-shortcuts.c | 683 ++++ src/global-shortcuts.h | 27 + src/inhibit.c | 539 +++ src/inhibit.h | 27 + src/input-capture.c | 1199 +++++++ src/input-capture.h | 25 + src/location.c | 742 ++++ src/location.h | 28 + src/memory-monitor.c | 116 + src/memory-monitor.h | 25 + src/meson.build | 189 ++ src/network-monitor.c | 244 ++ src/network-monitor.h | 25 + src/notification.c | 600 ++++ src/notification.h | 27 + src/open-uri.c | 1133 +++++++ src/open-uri.h | 28 + src/org.freedesktop.GeoClue2.Client.xml | 122 + src/org.freedesktop.portal.Desktop.service.in | 5 + src/permissions.c | 182 + src/permissions.h | 57 + src/pipewire.c | 356 ++ src/pipewire.h | 78 + src/portal-impl.c | 571 ++++ src/portal-impl.h | 40 + src/power-profile-monitor.c | 115 + src/power-profile-monitor.h | 25 + src/print.c | 325 ++ src/print.h | 27 + src/proxy-resolver.c | 112 + src/proxy-resolver.h | 26 + src/realtime.c | 310 ++ src/realtime.h | 25 + src/remote-desktop.c | 1684 ++++++++++ src/remote-desktop.h | 43 + src/request.c | 531 +++ src/request.h | 86 + src/restore-token.c | 378 +++ src/restore-token.h | 69 + src/rewrite-launchers.c | 281 ++ src/screen-cast.c | 1165 +++++++ src/screen-cast.h | 43 + src/screenshot.c | 501 +++ src/screenshot.h | 27 + src/sd-escape.c | 356 ++ src/sd-escape.h | 18 + src/secret.c | 217 ++ src/secret.h | 26 + src/session.c | 549 +++ src/session.h | 112 + src/settings.c | 220 ++ src/settings.h | 27 + src/trash.c | 145 + src/trash.h | 25 + src/validate-icon.c | 286 ++ src/wallpaper.c | 414 +++ src/wallpaper.h | 27 + ...xdg-desktop-portal-rewrite-launchers.service.in | 13 + src/xdg-desktop-portal.c | 489 +++ src/xdg-desktop-portal.gresource.xml | 8 + src/xdg-desktop-portal.service.in | 9 + src/xdp-utils.c | 2409 +++++++++++++ src/xdp-utils.h | 208 ++ tests/__init__.py | 512 +++ tests/account.c | 330 ++ tests/account.h | 8 + tests/backend/access.c | 171 + tests/backend/access.h | 3 + tests/backend/account.c | 186 + tests/backend/account.h | 3 + tests/backend/appchooser.c | 183 + tests/backend/appchooser.h | 3 + tests/backend/background.c | 99 + tests/backend/background.h | 3 + tests/backend/email.c | 212 ++ tests/backend/email.h | 3 + tests/backend/filechooser.c | 243 ++ tests/backend/filechooser.h | 3 + tests/backend/inhibit.c | 460 +++ tests/backend/inhibit.h | 3 + tests/backend/lockdown.c | 43 + tests/backend/lockdown.h | 5 + tests/backend/notification.c | 121 + tests/backend/notification.h | 3 + tests/backend/print.c | 260 ++ tests/backend/print.h | 3 + tests/backend/request.c | 119 + tests/backend/request.h | 54 + tests/backend/screenshot.c | 198 ++ tests/backend/screenshot.h | 3 + tests/backend/session.c | 195 ++ tests/backend/session.h | 56 + tests/backend/settings.c | 31 + tests/backend/settings.h | 3 + tests/backend/test-backends.c | 159 + tests/backend/wallpaper.c | 170 + tests/backend/wallpaper.h | 3 + tests/background.c | 208 ++ tests/background.h | 7 + tests/camera.c | 350 ++ tests/camera.h | 10 + tests/can-use-fuse.c | 138 + tests/can-use-fuse.h | 12 + tests/conftest.py | 78 + tests/dbs/meson.build | 5 + tests/dbs/no_tables | Bin 0 -> 32 bytes tests/email.c | 420 +++ tests/email.h | 9 + tests/filechooser.c | 945 ++++++ tests/filechooser.h | 25 + tests/glib-backports.c | 24 + tests/glib-backports.h | 8 + tests/inhibit.c | 404 +++ tests/inhibit.h | 10 + tests/limited-portals.c | 492 +++ tests/location.c | 107 + tests/location.h | 4 + tests/meson.build | 373 ++ tests/notification.c | 256 ++ tests/notification.h | 8 + tests/openuri.c | 480 +++ tests/openuri.h | 10 + tests/portals/limited/limited-portals.conf | 6 + tests/portals/limited/limited.portal.in | 3 + tests/portals/limited/meson.build | 13 + tests/portals/meson.build | 25 + tests/portals/test/meson.build | 13 + tests/portals/test/test-portals.conf | 2 + tests/portals/test/test.portal.in | 3 + tests/print.c | 503 +++ tests/print.h | 16 + tests/screenshot.c | 503 +++ tests/screenshot.h | 13 + tests/services/meson.build | 11 + tests/session.conf.in | 56 + tests/share/applications/furrfix.desktop | 13 + tests/share/applications/meson.build | 2 + tests/share/applications/mimeinfo.cache | 9 + tests/share/meson.build | 1 + tests/template.test.in | 4 + tests/templates/__init__.py | 227 ++ tests/templates/clipboard.py | 149 + tests/templates/email.py | 70 + tests/templates/globalshortcuts.py | 162 + tests/templates/inputcapture.py | 297 ++ tests/templates/meson.build | 16 + tests/templates/remotedesktop.py | 185 + tests/test-doc-portal.c | 879 +++++ tests/test-document-fuse.py | 1067 ++++++ tests/test-document-fuse.sh | 102 + tests/test-permission-store.c | 692 ++++ tests/test-portals.c | 599 ++++ tests/test-xdp-utils.c | 199 ++ tests/test_clipboard.py | 105 + tests/test_email.py | 234 ++ tests/test_globalshortcuts.py | 217 ++ tests/test_inputcapture.py | 616 ++++ tests/test_remotedesktop.py | 220 ++ tests/test_trash.py | 42 + tests/testdb.c | 360 ++ tests/trash.c | 47 + tests/trash.h | 3 + tests/utils.c | 61 + tests/utils.h | 11 + tests/wallpaper.c | 294 ++ tests/wallpaper.h | 7 + 327 files changed, 69514 insertions(+) create mode 100644 .github/workflows/check.yml create mode 100644 .github/workflows/docs.yml create mode 100644 CONTRIBUTING.md create mode 100644 COPYING create mode 100644 NEWS create mode 100644 README.md create mode 100644 RELEASE_HOWTO.md create mode 100644 SECURITY.md create mode 100644 data/meson.build create mode 100644 data/org.freedesktop.background.Monitor.xml create mode 100644 data/org.freedesktop.impl.portal.Access.xml create mode 100644 data/org.freedesktop.impl.portal.Account.xml create mode 100644 data/org.freedesktop.impl.portal.AppChooser.xml create mode 100644 data/org.freedesktop.impl.portal.Background.xml create mode 100644 data/org.freedesktop.impl.portal.Clipboard.xml create mode 100644 data/org.freedesktop.impl.portal.DynamicLauncher.xml create mode 100644 data/org.freedesktop.impl.portal.Email.xml create mode 100644 data/org.freedesktop.impl.portal.FileChooser.xml create mode 100644 data/org.freedesktop.impl.portal.GlobalShortcuts.xml create mode 100644 data/org.freedesktop.impl.portal.Inhibit.xml create mode 100644 data/org.freedesktop.impl.portal.InputCapture.xml create mode 100644 data/org.freedesktop.impl.portal.Lockdown.xml create mode 100644 data/org.freedesktop.impl.portal.Notification.xml create mode 100644 data/org.freedesktop.impl.portal.PermissionStore.xml create mode 100644 data/org.freedesktop.impl.portal.Print.xml create mode 100644 data/org.freedesktop.impl.portal.RemoteDesktop.xml create mode 100644 data/org.freedesktop.impl.portal.Request.xml create mode 100644 data/org.freedesktop.impl.portal.ScreenCast.xml create mode 100644 data/org.freedesktop.impl.portal.Screenshot.xml create mode 100644 data/org.freedesktop.impl.portal.Secret.xml create mode 100644 data/org.freedesktop.impl.portal.Session.xml create mode 100644 data/org.freedesktop.impl.portal.Settings.xml create mode 100644 data/org.freedesktop.impl.portal.Wallpaper.xml create mode 100644 data/org.freedesktop.portal.Account.xml create mode 100644 data/org.freedesktop.portal.Background.xml create mode 100644 data/org.freedesktop.portal.Camera.xml create mode 100644 data/org.freedesktop.portal.Clipboard.xml create mode 100644 data/org.freedesktop.portal.Device.xml create mode 100644 data/org.freedesktop.portal.Documents.xml create mode 100644 data/org.freedesktop.portal.DynamicLauncher.xml create mode 100644 data/org.freedesktop.portal.Email.xml create mode 100644 data/org.freedesktop.portal.FileChooser.xml create mode 100644 data/org.freedesktop.portal.FileTransfer.xml create mode 100644 data/org.freedesktop.portal.GameMode.xml create mode 100644 data/org.freedesktop.portal.GlobalShortcuts.xml create mode 100644 data/org.freedesktop.portal.Inhibit.xml create mode 100644 data/org.freedesktop.portal.InputCapture.xml create mode 100644 data/org.freedesktop.portal.Location.xml create mode 100644 data/org.freedesktop.portal.MemoryMonitor.xml create mode 100644 data/org.freedesktop.portal.NetworkMonitor.xml create mode 100644 data/org.freedesktop.portal.Notification.xml create mode 100644 data/org.freedesktop.portal.OpenURI.xml create mode 100644 data/org.freedesktop.portal.PowerProfileMonitor.xml create mode 100644 data/org.freedesktop.portal.Print.xml create mode 100644 data/org.freedesktop.portal.ProxyResolver.xml create mode 100644 data/org.freedesktop.portal.Realtime.xml create mode 100644 data/org.freedesktop.portal.RemoteDesktop.xml create mode 100644 data/org.freedesktop.portal.Request.xml create mode 100644 data/org.freedesktop.portal.ScreenCast.xml create mode 100644 data/org.freedesktop.portal.Screenshot.xml create mode 100644 data/org.freedesktop.portal.Secret.xml create mode 100644 data/org.freedesktop.portal.Session.xml create mode 100644 data/org.freedesktop.portal.Settings.xml create mode 100644 data/org.freedesktop.portal.Trash.xml create mode 100644 data/org.freedesktop.portal.Wallpaper.xml create mode 100644 doc/docbook.css create mode 100644 doc/meson.build create mode 100644 doc/portal-docs.xml.in create mode 100644 doc/portals-conf.rst create mode 100644 doc/redirect.html create mode 100644 doc/xmlto-config.xsl create mode 100644 document-portal/document-enums.h create mode 100644 document-portal/document-portal-fuse.c create mode 100644 document-portal/document-portal-fuse.h create mode 100644 document-portal/document-portal.c create mode 100644 document-portal/document-portal.h create mode 100644 document-portal/document-store.c create mode 100644 document-portal/document-store.h create mode 100644 document-portal/file-transfer.c create mode 100644 document-portal/file-transfer.h create mode 100644 document-portal/gvdb/README create mode 100644 document-portal/gvdb/gvdb-builder.c create mode 100644 document-portal/gvdb/gvdb-builder.h create mode 100644 document-portal/gvdb/gvdb-format.h create mode 100644 document-portal/gvdb/gvdb-reader.c create mode 100644 document-portal/gvdb/gvdb-reader.h create mode 100644 document-portal/gvdb/gvdb.doap create mode 100644 document-portal/meson.build create mode 100644 document-portal/org.freedesktop.impl.portal.PermissionStore.service.in create mode 100644 document-portal/org.freedesktop.portal.Documents.service.in create mode 100644 document-portal/permission-db.c create mode 100644 document-portal/permission-db.h create mode 100644 document-portal/permission-store.c create mode 100644 document-portal/xdg-document-portal.service.in create mode 100644 document-portal/xdg-permission-store.c create mode 100644 document-portal/xdg-permission-store.h create mode 100644 document-portal/xdg-permission-store.service.in create mode 100644 meson.build create mode 100644 meson_options.txt create mode 100644 po/LINGUAS create mode 100644 po/POTFILES.in create mode 100644 po/be.po create mode 100644 po/ca.po create mode 100644 po/cs.po create mode 100644 po/da.po create mode 100644 po/de.po create mode 100644 po/en_GB.po create mode 100644 po/es.po create mode 100644 po/fr.po create mode 100644 po/gl.po create mode 100644 po/he.po create mode 100644 po/hi.po create mode 100644 po/hr.po create mode 100644 po/hu.po create mode 100644 po/id.po create mode 100644 po/it.po create mode 100644 po/ka.po create mode 100644 po/lt.po create mode 100644 po/meson.build create mode 100644 po/nl.po create mode 100644 po/oc.po create mode 100644 po/pl.po create mode 100644 po/pt.po create mode 100644 po/pt_BR.po create mode 100644 po/ro.po create mode 100644 po/ru.po create mode 100644 po/sk.po create mode 100644 po/sr.po create mode 100644 po/sv.po create mode 100644 po/tr.po create mode 100644 po/uk.po create mode 100644 po/zh_CN.po create mode 100644 po/zh_TW.po create mode 100644 src/account.c create mode 100644 src/account.h create mode 100644 src/background-monitor.c create mode 100644 src/background-monitor.h create mode 100644 src/background.c create mode 100644 src/background.h create mode 100644 src/call.c create mode 100644 src/call.h create mode 100644 src/camera.c create mode 100644 src/camera.h create mode 100644 src/clipboard.c create mode 100644 src/clipboard.h create mode 100644 src/device.c create mode 100644 src/device.h create mode 100644 src/documents.c create mode 100644 src/documents.h create mode 100644 src/dynamic-launcher.c create mode 100644 src/dynamic-launcher.h create mode 100644 src/email.c create mode 100644 src/email.h create mode 100644 src/file-chooser.c create mode 100644 src/file-chooser.h create mode 100644 src/flatpak-instance.c create mode 100644 src/flatpak-instance.h create mode 100644 src/gamemode.c create mode 100644 src/gamemode.h create mode 100644 src/glib-backports.c create mode 100644 src/glib-backports.h create mode 100644 src/global-shortcuts.c create mode 100644 src/global-shortcuts.h create mode 100644 src/inhibit.c create mode 100644 src/inhibit.h create mode 100644 src/input-capture.c create mode 100644 src/input-capture.h create mode 100644 src/location.c create mode 100644 src/location.h create mode 100644 src/memory-monitor.c create mode 100644 src/memory-monitor.h create mode 100644 src/meson.build create mode 100644 src/network-monitor.c create mode 100644 src/network-monitor.h create mode 100644 src/notification.c create mode 100644 src/notification.h create mode 100644 src/open-uri.c create mode 100644 src/open-uri.h create mode 100644 src/org.freedesktop.GeoClue2.Client.xml create mode 100644 src/org.freedesktop.portal.Desktop.service.in create mode 100644 src/permissions.c create mode 100644 src/permissions.h create mode 100644 src/pipewire.c create mode 100644 src/pipewire.h create mode 100644 src/portal-impl.c create mode 100644 src/portal-impl.h create mode 100644 src/power-profile-monitor.c create mode 100644 src/power-profile-monitor.h create mode 100644 src/print.c create mode 100644 src/print.h create mode 100644 src/proxy-resolver.c create mode 100644 src/proxy-resolver.h create mode 100644 src/realtime.c create mode 100644 src/realtime.h create mode 100644 src/remote-desktop.c create mode 100644 src/remote-desktop.h create mode 100644 src/request.c create mode 100644 src/request.h create mode 100644 src/restore-token.c create mode 100644 src/restore-token.h create mode 100644 src/rewrite-launchers.c create mode 100644 src/screen-cast.c create mode 100644 src/screen-cast.h create mode 100644 src/screenshot.c create mode 100644 src/screenshot.h create mode 100644 src/sd-escape.c create mode 100644 src/sd-escape.h create mode 100644 src/secret.c create mode 100644 src/secret.h create mode 100644 src/session.c create mode 100644 src/session.h create mode 100644 src/settings.c create mode 100644 src/settings.h create mode 100644 src/trash.c create mode 100644 src/trash.h create mode 100644 src/validate-icon.c create mode 100644 src/wallpaper.c create mode 100644 src/wallpaper.h create mode 100644 src/xdg-desktop-portal-rewrite-launchers.service.in create mode 100644 src/xdg-desktop-portal.c create mode 100644 src/xdg-desktop-portal.gresource.xml create mode 100644 src/xdg-desktop-portal.service.in create mode 100644 src/xdp-utils.c create mode 100644 src/xdp-utils.h create mode 100644 tests/__init__.py create mode 100644 tests/account.c create mode 100644 tests/account.h create mode 100644 tests/backend/access.c create mode 100644 tests/backend/access.h create mode 100644 tests/backend/account.c create mode 100644 tests/backend/account.h create mode 100644 tests/backend/appchooser.c create mode 100644 tests/backend/appchooser.h create mode 100644 tests/backend/background.c create mode 100644 tests/backend/background.h create mode 100644 tests/backend/email.c create mode 100644 tests/backend/email.h create mode 100644 tests/backend/filechooser.c create mode 100644 tests/backend/filechooser.h create mode 100644 tests/backend/inhibit.c create mode 100644 tests/backend/inhibit.h create mode 100644 tests/backend/lockdown.c create mode 100644 tests/backend/lockdown.h create mode 100644 tests/backend/notification.c create mode 100644 tests/backend/notification.h create mode 100644 tests/backend/print.c create mode 100644 tests/backend/print.h create mode 100644 tests/backend/request.c create mode 100644 tests/backend/request.h create mode 100644 tests/backend/screenshot.c create mode 100644 tests/backend/screenshot.h create mode 100644 tests/backend/session.c create mode 100644 tests/backend/session.h create mode 100644 tests/backend/settings.c create mode 100644 tests/backend/settings.h create mode 100644 tests/backend/test-backends.c create mode 100644 tests/backend/wallpaper.c create mode 100644 tests/backend/wallpaper.h create mode 100644 tests/background.c create mode 100644 tests/background.h create mode 100644 tests/camera.c create mode 100644 tests/camera.h create mode 100644 tests/can-use-fuse.c create mode 100644 tests/can-use-fuse.h create mode 100644 tests/conftest.py create mode 100644 tests/dbs/meson.build create mode 100644 tests/dbs/no_tables create mode 100644 tests/email.c create mode 100644 tests/email.h create mode 100644 tests/filechooser.c create mode 100644 tests/filechooser.h create mode 100644 tests/glib-backports.c create mode 100644 tests/glib-backports.h create mode 100644 tests/inhibit.c create mode 100644 tests/inhibit.h create mode 100644 tests/limited-portals.c create mode 100644 tests/location.c create mode 100644 tests/location.h create mode 100644 tests/meson.build create mode 100644 tests/notification.c create mode 100644 tests/notification.h create mode 100644 tests/openuri.c create mode 100644 tests/openuri.h create mode 100644 tests/portals/limited/limited-portals.conf create mode 100644 tests/portals/limited/limited.portal.in create mode 100644 tests/portals/limited/meson.build create mode 100644 tests/portals/meson.build create mode 100644 tests/portals/test/meson.build create mode 100644 tests/portals/test/test-portals.conf create mode 100644 tests/portals/test/test.portal.in create mode 100644 tests/print.c create mode 100644 tests/print.h create mode 100644 tests/screenshot.c create mode 100644 tests/screenshot.h create mode 100644 tests/services/meson.build create mode 100644 tests/session.conf.in create mode 100644 tests/share/applications/furrfix.desktop create mode 100644 tests/share/applications/meson.build create mode 100644 tests/share/applications/mimeinfo.cache create mode 100644 tests/share/meson.build create mode 100644 tests/template.test.in create mode 100644 tests/templates/__init__.py create mode 100644 tests/templates/clipboard.py create mode 100644 tests/templates/email.py create mode 100644 tests/templates/globalshortcuts.py create mode 100644 tests/templates/inputcapture.py create mode 100644 tests/templates/meson.build create mode 100644 tests/templates/remotedesktop.py create mode 100644 tests/test-doc-portal.c create mode 100755 tests/test-document-fuse.py create mode 100755 tests/test-document-fuse.sh create mode 100644 tests/test-permission-store.c create mode 100644 tests/test-portals.c create mode 100644 tests/test-xdp-utils.c create mode 100644 tests/test_clipboard.py create mode 100644 tests/test_email.py create mode 100644 tests/test_globalshortcuts.py create mode 100644 tests/test_inputcapture.py create mode 100644 tests/test_remotedesktop.py create mode 100644 tests/test_trash.py create mode 100644 tests/testdb.c create mode 100644 tests/trash.c create mode 100644 tests/trash.h create mode 100644 tests/utils.c create mode 100644 tests/utils.h create mode 100644 tests/wallpaper.c create mode 100644 tests/wallpaper.h diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..f2c6a2d --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,130 @@ +name: Portal CI + +on: [push, pull_request] + +env: + DEBIAN_FRONTEND: noninteractive + TESTS_TIMEOUT: 10 # in minutes + +jobs: + check: + name: Ubuntu 22.04 build + runs-on: ubuntu-22.04 + strategy: + matrix: + compiler: ['gcc', 'clang'] + sanitizer: ['address'] + + env: + UBUNTU_VERSION: '22.04' + CC: ${{ matrix.compiler }} + BASE_CFLAGS: -Wp,-D_FORTIFY_SOURCE=2 + BUILD_CONTAINER: ${{ matrix.compiler }}-build-container + RUN_CMD: docker exec -t -w /src -e TEST_IN_CI -e ASAN_OPTIONS -e G_MESSAGES_DEBUG -e XDG_DATA_DIRS ${{ matrix.compiler }}-build-container + AS_USER: runuser -u tester -- + BUILDDIR: builddir + + steps: + - name: Prepare environment + id: env-setup + run: | + echo "cflags=$BASE_CFLAGS" >> $GITHUB_OUTPUT; + + - name: Prepare container + run: | + docker run --name $BUILD_CONTAINER \ + --tty --device /dev/fuse --cap-add SYS_ADMIN \ + --security-opt apparmor:unconfined \ + -v $(pwd):/src \ + -e DEBIAN_FRONTEND \ + -e DEBCONF_NONINTERACTIVE_SEEN=true \ + -e TERM=dumb \ + -e MAKEFLAGS="-j $(getconf _NPROCESSORS_ONLN)" \ + -e CC -e CFLAGS="${{ steps.env-setup.outputs.cflags }}" \ + -d ubuntu:$UBUNTU_VERSION sleep infinity + + - name: Install dependencies + run: | + $RUN_CMD apt-get update --quiet + $RUN_CMD apt-get upgrade --quiet -y + $RUN_CMD apt-get install --quiet -y --no-install-recommends \ + ${{ matrix.compiler }} \ + desktop-file-utils \ + fuse3 \ + gettext \ + gnome-desktop-testing \ + gtk-doc-tools \ + libcap2-bin \ + libflatpak-dev \ + libfontconfig1-dev \ + libfuse3-dev \ + libgdk-pixbuf-2.0-dev \ + libgeoclue-2-dev \ + libglib2.0-dev \ + libjson-glib-dev \ + libpipewire-0.3-dev \ + libportal-dev \ + libsystemd-dev \ + libtool \ + llvm \ + python3-gi \ + shared-mime-info + + - name: Install dependencies for meson + run: | + $RUN_CMD apt-get install --quiet -y --no-install-recommends \ + meson + + - name: Install dependencies for the pytest test suite + run: | + $RUN_CMD apt-get install --quiet -y --no-install-recommends \ + python3-pytest python3-pytest-xdist python3-dbusmock python3-dbus + + - name: Check out xdg-desktop-portal + uses: actions/checkout@v3 + + - name: Setup test user + run: | + $RUN_CMD adduser --disabled-password --gecos "" tester + $RUN_CMD chown tester:tester . -R + + - name: Build xdg-desktop-portal + run: | + $RUN_CMD $AS_USER meson setup ${BUILDDIR} $MESON_OPTIONS + $RUN_CMD $AS_USER meson compile -C ${BUILDDIR} + env: + MESON_OPTIONS: -Dinstalled-tests=true -Dpytest=enabled -Db_sanitize=${{ matrix.sanitizer }} + + - name: Run xdg-desktop-portal tests + run: $RUN_CMD $AS_USER timeout --signal=KILL -v ${TESTS_TIMEOUT}m meson test -C ${BUILDDIR} + env: + TEST_IN_CI: 1 + G_MESSAGES_DEBUG: all + ASAN_OPTIONS: detect_leaks=0 # Right now we're not fully clean, but this gets us use-after-free etc + + - name: Install xdg-desktop-portal + run: $RUN_CMD meson install -C ${BUILDDIR} + + - name: Run xdg-desktop-portal installed-tests + run: | + test -n "$($RUN_CMD $AS_USER gnome-desktop-testing-runner -l xdg-desktop-portal)" + $RUN_CMD $AS_USER \ + env TEST_INSTALLED_IN_CI=1 XDG_DATA_DIRS=/src/tests/share/:$XDG_DATA_DIRS \ + gnome-desktop-testing-runner --report-directory installed-test-logs/ \ + -t $((TESTS_TIMEOUT * 60)) xdg-desktop-portal + env: + G_MESSAGES_DEBUG: all + TEST_IN_CI: 1 + XDG_DATA_DIRS: /usr/local/share:/usr/share + ASAN_OPTIONS: detect_leaks=0 # Right now we're not fully clean, but this gets us use-after-free etc + + - name: Upload test logs + uses: actions/upload-artifact@v3 + if: success() || failure() + with: + name: test logs + path: | + tests/*.log + test-*.log + installed-test-logs/ + builddir/meson-logs/testlog.txt diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..d6f5704 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,71 @@ +name: Documentation + +on: [push, pull_request] + +jobs: + docs: + runs-on: ubuntu-20.04 + container: + image: ubuntu:22.04 + steps: + - name: Install dependencies + env: + DEBIAN_FRONTEND: noninteractive + run: | + apt-get update + apt-get upgrade -y + apt-get install -y --no-install-recommends \ + desktop-file-utils \ + fuse3 \ + gcc \ + gettext \ + gnome-desktop-testing \ + gtk-doc-tools \ + libcap2-bin \ + libflatpak-dev \ + libfontconfig1-dev \ + libfuse3-dev \ + libgdk-pixbuf-2.0-dev \ + libgeoclue-2-dev \ + libglib2.0-dev \ + libjson-glib-dev \ + libpipewire-0.3-dev \ + libportal-dev \ + libsystemd-dev \ + llvm \ + meson \ + python3-gi \ + shared-mime-info + + - name: Install dependencies for doc builds + env: + DEBIAN_FRONTEND: noninteractive + run: | + apt-get install -y --no-install-recommends \ + ca-certificates \ + git \ + xmlto + + - name: Check out xdg-desktop-portal + uses: actions/checkout@v3 + + - name: Build docs + run: | + meson setup builddir -Ddocbook-docs=enabled + meson compile -C builddir + + - name: Prepare docs + working-directory: builddir/doc + run: | + mkdir published-docs + mv portal-docs.html ./published-docs/index.html + mv redirect.html ./published-docs/portal-docs.html + mv docbook.css ./published-docs + + - name: Deploy docs + uses: peaceiris/actions-gh-pages@v3 + if: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/main') }} + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./builddir/doc/published-docs/ + destination_dir: ./ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0540ba3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,58 @@ +# XDG Desktop Portal - Contributing Guide + +Before developing features or fixing bugs, please make sure you are have done +the following: + +- Your code is not on the *main* branch of your fork +- The code has been tested +- All commit messages are properly formatted and commits squashed where + appropriate +- You have included updates to all appropriate documentation + +We use GitHub pull requests to review contributions. Please be kind and patient +as code reviews can be long and minutious. + +## Development + +xdg-desktop-portal usually runs as a user session service, initialized on +demand through D-Bus activation. It usually starts with the session though, +as many desktop environments try to talk to xdg-desktop-portal on startup. +xdg-desktop-portal initializes specific backends through D-Bus activation +as well. + +### Building + +To build xdg-desktop-portal, first make sure you have the build dependencies +installed through your distribution's package manager. With them installed, +run: + +``` +$ meson setup . _build +$ meson compile -C _build +``` + +### Running + +xdg-desktop-portal needs to own the D-Bus name and replace the user session +service that might already be running. To do so, run: + +``` +$ _build/src/xdg-desktop-portal --replace +``` + +You may need to restart backends after replacing xdg-desktop-portal (please +replace `[name]` with the backend name, e.g. `gnome` or `kde` or `wlr`): + +``` +$ systemctl --user restart xdg-desktop-portal-[name].service +``` + +### Testing + +To execute the test suite present in xdg-desktop-portal, make sure you built it +with `-Dlibportal=enabled`, and run: + +``` +$ meson test -C _build +``` + diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..4362b49 --- /dev/null +++ b/COPYING @@ -0,0 +1,502 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..c411161 --- /dev/null +++ b/NEWS @@ -0,0 +1,500 @@ +Changes in 1.17.0 +================= +Released: 2023-08-04 + +- Drop the Autotools build. Meson is now the only supported build system. +- Rework how portal implementations are loaded. This new, more robust system + allows selecting specific backends for specific portals, and layering them + when necessary. Platforms that provide portals implementation are encouraged + to provide a suitable configuration file. +- Introduce a new Clipboard portal. This portal extends the Remote Desktop + portal by adding support for sharing clipboard between remote machines. +- Introduce a new Input Capture portal. This portal adds mechanisms for taking + control of input devices. The primary usage model is centered around the + InputLeap and Synergy use cases, where local devices are used to control + remote displays. +- Stop using the deprecated GTimeVal struct +- Bump GLib dependency to 2.66 +- Add an "accept-label" option the the Print portal. This lets apps suggest a + proper label to the print operation. +- Various fixes to the Global Shortcuts portal +- Support restoring remote desktop sessions +- Improve robustness of the OpenURI portal by validating more URIs +- The PipeWire dependency is now mandatory +- Various improvements for the test suite +- Translation updates + +Changes in 1.16.0 +================= +Released: 2022-12-12 + +- Introduce a new background monitoring service. This allows desktop + environments to list applications that are running in background, that is, + sandboxed applications running without a visible window. Desktop environments + can display these background running applications in their interfaces, and + allow users to control their execution. +- Introduce the Global Shortcuts portal. This portal allows applications to + register and receive keyboard shortcuts even when they're not focused. This + was a highly requested feature, especially on Wayland desktops. There are + improvements to come, but portal backends can now implement this new portal. +- Various CI improvements +- Translation updates + +Changes in 1.15.0 +================= +Released: 2022-08-11 + +- Add Meson build files. For now, both Autotools and Meson are available + in the source tree. Starting from next release, tarballs will be generated + using Meson. There is no set date to delete Autotools files, but it will + happen at some point in the future. The removal of Autotools will be + communicated in advance. +- Make the Screenshot portal request permission to take screenshots. Frontends + that implement the version 2 of org.freedesktop.portal.impl.Screenshot portal + can now be aware that the screenshot permission was granted through the new + 'permission_store_checked' option, and skip any kind of dialog when that is + the case. +- Stop sending the app id quoted in the Background portal +- Fix a bug in cgroup name parsing +- Various fixes to the Realtime portal +- Various CI improvements +- Translation updates + +Changes in 1.14.1 +================= +Released: 2022-03-18 + +- Fix an issue in 1.14.0 where xdg-desktop-portal.service starting before + graphical-session-pre.target would cause the GNOME session to deadlock by + moving code to a new libexec binary which handles deleting or migrating + .desktop files (and icons) from the dynamic launcher portal for apps which + have been uninstalled or renamed. +- Fix some bugs in the aforementioned launcher migration implementation, + "xdg-desktop-portal-rewrite-launchers". +- Fix build without libsystemd + +Changes in 1.14.0 +================= +Released: 2022-03-17 + +- Add a new "dynamic launcher" portal, which can install .desktop files and + accompanying icons after user confirmation. +- Rework handling of empty app IDs: In case an empty string app ID is stored in + the permission store, this permission is now shared only by apps whose app ID + couldn't be determined, rather than all unsandboxed apps. +- Use libsystemd (when available) to try to determine the app ID of unsandboxed + processes. This is useful since some portals otherwise can't be used by host + apps. +- Make x-d-p start on session start, which is needed for the dynamic launcher + portal to handle rewriting launchers for apps that have been renamed. +- Bring back the copy of Flatpak's icon-validator, which was dropped many + releases ago. +- Icon validation is now required for the notification and dynamic launcher + portals (previously it was only done if the "flatpak-validate-icon" binary + could be found). +- document-portal: Move to the libfuse3 API +- document-portal: Use renameat2 sys call +- document-portal: Use mutex to fix concurrency bug +- realtime: Fix error code paths +- realtime: Fix MakeThreadHighPriorityWithPID method +- screencast: Fix an error when restoring streams +- ci: Various improvements +- Documentation improvements +- New translations: pt, ro + +Changes in 1.12.1 +================= +Released: 2021-12-22 + +- Fix a crash in the device portal + +Changes in 1.12.0 +================= +Released: 2021-12-21 + +- Place portals in the systemd session.slice +- settings: Add color-scheme key +- open-uri: Avoid a sync call to org.freedesktop.FileManager +- screncast: Allow restoring previous sessions +- Add a portal for requesting realtime permissions +- ci: Many improvements +- Publish the docs from a ci job +- Translation updates + +Changes in 1.10.0 +================= + +- Remap /run/flatpak/app, for Flatpak 1.11.x +- Remap /var/config and /var/data +- permission-store: Avoid a crash +- permissions-store: Add GetPermission +- screencast: Add 'virtual' source type +- openuri: Use real path for OpenDirectory +- location: Fix accuracy levels +- Add power profile monitor implementation +- Translation updates + +Changes in 1.8.1 +================ + +- openuri: Fix an fd leak +- filechooser: Fix directory support +- build: Drop a fontconfig dependency +- snap: Use cgroups to identify confined apps +- documents: Add snap support +- wallpaper: Fix a crash +- build: Factor out a fuse check +- Translation updates + +Changes in 1.8.0 +================ + +- openuri: Allow skipping the chooser for more URL tyles +- openuri: Robustness fixes +- filechooser: Return the current filter +- camera: Make the client node visible +- camera: Don't leak pipewire proxy +- Fix file descriptor leaks +- Testsuite improvements +- Translation updates + +Changes in 1.7.2 +================ + +- document: Reduce the use of open fds +- document: Add more tests and fix issues they found +- Fix the build with musl + +Changes in 1.7.1 +================ + +- filechooser: Add a "directory" option +- filechooser: Document the "writable" option +- document: Expose directories with their proper name + +Changes in 1.7.0 +================ + +- testsuite improvements +- background: Avoid a segfault +- screencast: Require pipewire 0.3 +- document: Support exporting directories +- document: New fuse implementation +- better support for snap and toolbox +- Translation updates + +Changes in 1.6.0 +================ + +- tests: Adapt to libportal api changes + +Changes in 1.5.4 +================ + +- background: Add a signal to the impl api +- background: Rewrite the monitoring to better track when apps disappear +- permissions: Fix SetValue handling of GVariant wrapping. This is an api change +- openuri: Add a per-type always-ask option +- openuri: Show the app chooser dialog less often +- memorymonitor: A new portal to let apps receive low memory warnings +- filetransfer: A new portal to rewrite file paths between sandboxes + +Changes in 1.5.3 +================ + +* Add more tests +* Translation updates +* location: Various fixes +* document portal: Monitor fuse mount +* openuri: Only ask 3 times to use the same app +* openuri: Add an 'ask' option +* Fix build from git +* email: Allow multiple addresses, cc and bcc +* filechooser: Allow saving multiple files + +Changes in 1.5.2 +================ + +* Add many more tests, using libportal +* gamemode: Add a pidfd-based api +* inhibit: Send a Response signal +* openuri: Add an OpenDirectory api +* Translation updates + +Changes in 1.5.1 +================ + +* Add a portal for setting desktop backgrounds +* Add tests +* Optionally use libportal (for tests) + +Changes in 1.5.0 +================ + +* Add a secret portal that is meant be used via + libsecret inside the sandbox. One backend for + this will live in gnome-keyring, others are + possible +* Fix a file descriptor leak +* Reduce log spam +* Translation updates + +Changes in 1.4.2 +================ + +* Build fixes + +Changes in 1.4.0 +================ + +* Add a background&autostart portal +* Add a gamemode portal +* Add a camera portal +* Require pipewire 0.2.6 +* inhibit: Track session state +* documents: Fix a ref-counting bug +* screencast: Add cursor modes +* screencast: Memory leak fixes +* Translation updates + +Changes in 1.2.0 +================ + +* notification: Use icon validator from flatpak +* notification: Don't leave temp files around +* email: Validate options better +* inhibit: Validate options better +* file chooser: Add support for setting the current filter +* Translation updates + +Changes in 1.1.1 +================ + +* Validate icons in notifications +* Respect lockdown settings +* Write back permissions for notifications to indicate portal use +* Set st_nlink in the documents portal +* Add infrastructure for validating options +* Validate email addresses +* Translation updates + +Changes in 1.1.0 +================ + +This is the first release in the new unstable 1.1.x series, leading up to 1.2 +which is expected around the end of the year. + +* Add a location portal, this requires geoclue 2.5.2 +* Add a settings portal, for desktop-wide settings like XSettings or kdeglobals +* Allow locking down printing, save-to-disk and opening uris +* Monitor application changes in the open uri portal +* Add more tests + +xdg-desktop-portal 1.0.3 +======================== + +* Fix an option name in the remote desktop portal +* document-portal: Validate permissions and report errors +* Fix life-cycle issues with inodes in the document portal +* Improve the test coverage of the documents portal +* Add a 'coverage' make target + +xdg-desktop-portal 1.0.2 +======================== + +* networkmonitor: Fix several issues +* inhibit: Add session state monitoring + +xdg-desktop-portal 1.0.1 +======================== + +* networkmonitor: Add GetStatus and CanReach methods +* Unset GTK_USE_PORTAL +* Add a portal for moving files to the trash +* Fix an inode leak in the document portal + +xdg-desktop-portal 1.0 +====================== + +* screenshot: Add a color picker api +* screencast: Bump the pipewire dependency to 0.2.1 +* Improve --help output +* Small documentation improvements + +xdg-desktop-portal 0.99 +======================= + +* The NetworkMonitor portal API has been changed to allow access control +* The Proxy and NetworkMonitor portals only respond to requests from + sandboxes with network access +* The flatpak portal is now documented + +xdg-desktop-portal 0.11 +======================= + +* Add initial support for Snap packages. +* Fix memory leaks when ownership of bus names changes. +* Include docs for the session, remote desktop and screencast portals. +* document-portal: Be more flexible validating apps' IDs. +* document-portal: Be more strict when checking & granting file access. +* file-chooser: Fix crash with unitialized data in the save dialog. +* open-uri: Don't ever skip showing the dialog if a threshold is set. +* open-uri: Don't register http: URIs for sandboxed handlers. +* remote-desktop: Use the correct device type values. +* screencast: Fix synchronization issue with PipeWire. + +* Translation updates + Chinese (Taiwan) + Spanish + +xdg-desktop-portal 0.10 +======================= + +This version of xdg-desktop-portal contains the xdg-document-portal +that used to be shipped by flatpak. The code was moved to +xdg-desktop-portal as a first step towards being used by +snappy. Additionally having the two related portals delivered together +makes it easier to implement new features that rely on changes to +both portals. + +The two versions of the document portal are fully compatible, but the +package files will conflict with older versions of flatpak, so +packagers will have to pick one version. Following this there will be +a new release of unstable flatpak with the document portal removed, +and a release of the stable branch (0.10) that has a configure +option to disable the document portal. + +Additionally, this release contains a new screencast and remote +desktop portal based on PipeWire. + +Major changes in this versions: + + * Import permission store from flatpak + * Import document portal from flatpak + * Add remote desktop portal + * Add screencast portal + * Add "interactive" mode to screenshot portal + * file-chooser: Don't return document paths for paths the application has direct access to + * Handle newer version of bubblewrap + * New abstraction for application info, supporting multiple sandbox technologies + * Add basic test suite + +xdg-desktop-portal 0.9 +====================== + +* Install pkg-config files into datadir + +* Avoid a race in the portal APIs + +* Change the email portal to take fds + +* Translation updates + Galicican + Indonesian + Turkish + + +xdg-desktop-portal 0.8 +====================== + +* Update po files + +xdg-desktop-portal 0.7 +====================== + +* In the OpenURI portal, send the content-type and filename to the appchooser + +* Stop handling file: uris in the OpenURI method of the OpenURI portal + +* Add an OpenFile method for local files to the OpenURI portal + +* Bug fixes in the notification portal + +* Translation updates: + Czech + Italian + +xdg-desktop-portal 0.6 +====================== + +* A portal for sending email has been added + +* The OpenURI portal has be made a bit more permissive. + It will now directly use the system default for "safe" + schemes such as http, and local directories. + +* Translation updates: + French + +xdg-desktop-portal 0.5 +====================== + +* The notification portal now supports non-exported actions + +* An Account portal for basic user information has been added + +* All portal interface now have a version property. Currently, + all interfaces are at version 1 + +* The file chooser portal was forgetting to make files created + for SaveFile writable. This has been fixed + +* Translation updates: + Czech + Polish + Swedish + +xdg-desktop-portal 0.4 +====================== + +* No longer rely on cgroups to find the app id + +* Fix handling of mime type filters in the file chooser portal + +* Translation updates: + Chinese + Czech + German + Serbian + + +xdg-desktop-portal 0.3 +====================== + +* open-uri: Allow configuring threshold + +* open-uri: Use fallback applications when needed + +* Translation updates: + Brazilian Portuguese + Hungarian + Slovak + Ukrainian + + +xdg-desktop-portal 0.2 +====================== + +* Add internationalization support + +* Add Qt annotations + +* New portal APIs: + - org.freedesktop.portal.Device + +* Translation updates: + Polish + + +Initial release 0.1 +=================== + +Included portal APIs: + * org.freedesktop.portal.FileChooser + * org.freedesktop.portal.Print + * org.freedesktop.portal.OpenURI + * org.freedesktop.portal.Screenshot + * org.freedesktop.portal.Inhibit + * org.freedesktop.portal.Notification + * org.freedesktop.portal.NetworkMonitor + * org.freedesktop.portal.ProxyResolver diff --git a/README.md b/README.md new file mode 100644 index 0000000..a807afe --- /dev/null +++ b/README.md @@ -0,0 +1,89 @@ +# xdg-desktop-portal + +A portal frontend service for [Flatpak](http://www.flatpak.org) and possibly +other desktop containment frameworks. + +xdg-desktop-portal works by exposing a series of D-Bus interfaces known as +_portals_ under a well-known name (org.freedesktop.portal.Desktop) and object +path (/org/freedesktop/portal/desktop). + +The portal interfaces include APIs for file access, opening URIs, printing +and others. + +Documentation for the available D-Bus interfaces can be found +[here](https://flatpak.github.io/xdg-desktop-portal/portal-docs.html). + +## Building xdg-desktop-portal + +xdg-desktop-portal depends on GLib and Flatpak. +To build the documentation, you will need xmlto and the docbook stylesheets. +For more instructions, please read [CONTRIBUTING.md][contributing]. + +## Using portals + +Flatpak grants sandboxed applications _talk_ access to names in the +org.freedesktop.portal.\* prefix. One possible way to use the portal APIs +is thus just to make D-Bus calls. For many of the portals, toolkits (e.g. +GTK+) are expected to support portals transparently if you use suitable +high-level APIs. + +To implement most portals, xdg-desktop-portal relies on a backend +that provides implementations of the org.freedesktop.impl.portal.\* interfaces. + +Here are some examples of available backends: + +- GTK [xdg-desktop-portal-gtk](http://github.com/flatpak/xdg-desktop-portal-gtk) +- GNOME [xdg-desktop-portal-gnome](https://gitlab.gnome.org/GNOME/xdg-desktop-portal-gnome/) +- KDE [xdg-desktop-portal-kde](https://invent.kde.org/plasma/xdg-desktop-portal-kde) +- LXQt [xdg-desktop-portal-lxqt](https://github.com/lxqt/xdg-desktop-portal-lxqt) +- Pantheon (Elementary) [xdg-desktop-portal-pantheon](https://github.com/elementary/portals) +- wlroots [xdg-desktop-portal-wlr](https://github.com/emersion/xdg-desktop-portal-wlr) +- Deepin [xdg-desktop-portal-dde](https://github.com/linuxdeepin/xdg-desktop-portal-dde) +- Xapp (Cinnamon, MATE, Xfce) [xdg-desktop-portal-xapp](https://github.com/linuxmint/xdg-desktop-portal-xapp) + +## Design considerations + +There are several reasons for the frontend/backend separation of the portal +code: +- We want to have _native_ portal dialogs that match the session desktop (i.e. + GTK+ dialogs for GNOME, Qt dialogs for KDE) +- One of the limitations of the D-Bus proxying in flatpak is that allowing a + sandboxed app to talk to a name implicitly also allows it to talk to any other + name owned by the same unique name. Therefore, sandbox-facing D-Bus apis + should generally be hosted on a dedicated bus connection. For portals, the + frontend takes care of this for us. +- The frontend can handle all the interaction with _portal infrastructure_, such + as the permission store and the document store, freeing the backends to focus + on just providing a user interface. +- The frontend can also handle argument validation, and be strict about only + letting valid requests through to the backend. + +The portal apis are all following the pattern of an initial method call, whose +response returns an object handle for an _org.freedesktop.portal.Request_ object +that represents the portal interaction. The end of the interaction is done via a +_Response_ signal that gets emitted on that object. This pattern was chosen over +a simple method call with return, since portal apis are expected to show dialogs +and interact with the user, which may well take longer than the maximum method +call timeout of D-Bus. Another advantage is that the caller can cancel an +ongoing interaction by calling the _Cancel_ method on the request object. + +One consideration for deciding the shape of portal APIs is that we want them to +'hide' behind existing library APIs where possible, to make it as easy as +possible to have apps use them _transparently_. For example, the OpenFile portal +is working well as a backend for the GtkFileChooserNative API. + +When it comes to files, we need to be careful to not let portal apis subvert the +limited filesystem view that apps have in their sandbox. Therefore, files should +only be passed into portal APIs in one of two forms: +- As a document ID referring to a file that has been exported in the document + portal +- As an open fd. The portal can work its way back to a file path from the fd, + and passing an fd proves that the app inside the sandbox has access to the + file to open it. + +When it comes to processes, passing pids around is not useful in a sandboxed +world where apps are likely in their own pid namespace. And passing pids from +inside the sandbox is problematic, since the app can just lie. + + +[contributing]: https://github.com/flatpak/xdg-desktop-portal/blob/main/CONTRIBUTING.md diff --git a/RELEASE_HOWTO.md b/RELEASE_HOWTO.md new file mode 100644 index 0000000..cd79ebc --- /dev/null +++ b/RELEASE_HOWTO.md @@ -0,0 +1,19 @@ +# Steps for doing a xdg-desktop-portal release + + - git clean -fxd + - meson setup . _build && meson compile -C _build/ xdg-desktop-portal-update-po + - git add po/*po && git commit -m "Update po files" + - git clean -fxd + - add content to NEWS + - git commit -m + - git push origin main + - meson setup . _build -Ddocbook-docs=enabled + - meson compile -C _build dist + - git tag + - git push origin refs/tags/ + - upload tarball to github as release + - edit release, copy NEWS section in + - update portal api docs in the gh-pages branch + - bump version in configure.ac and meson.build + - git commit -m "Post-release version bump" + - git push origin main diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..5ead55b --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,16 @@ +# Security policy for xdg-desktop-portal + +See the [Flatpak security policy](https://github.com/flatpak/flatpak/blob/main/SECURITY.md) + +## Supported Versions + +In stable branches and released packages, this table is likely to be outdated; +please check +[the latest version](https://github.com/flatpak/xdg-desktop-portal/blob/main/SECURITY.md). + +| Version | Supported | Status +| -------- | ------------------ | ------------------------------------------------------------- | +| 1.17.x | :white_check_mark: | Development branch, releases may include non-security changes | +| 1.16.x | :white_check_mark: | Stable branch, recommended for use in distributions | +| 1.14.x | :white_check_mark: | Old stable branch, still maintained | +| <= 1.12.x | :x: | Older branches, no longer supported | diff --git a/data/meson.build b/data/meson.build new file mode 100644 index 0000000..eea49ad --- /dev/null +++ b/data/meson.build @@ -0,0 +1,74 @@ +# This file is built into the GDbus sources but does not get installed by us +flatpak_intf_sources = [] +if flatpak_intf_dir != '' + flatpak_intf_sources = files(flatpak_intf_dir / 'org.freedesktop.portal.Flatpak.xml') +endif + +portal_sources = files( + 'org.freedesktop.portal.Account.xml', + 'org.freedesktop.portal.Background.xml', + 'org.freedesktop.portal.Camera.xml', + 'org.freedesktop.portal.Clipboard.xml', + 'org.freedesktop.portal.Device.xml', + 'org.freedesktop.portal.Documents.xml', + 'org.freedesktop.portal.DynamicLauncher.xml', + 'org.freedesktop.portal.Email.xml', + 'org.freedesktop.portal.FileChooser.xml', + 'org.freedesktop.portal.FileTransfer.xml', + 'org.freedesktop.portal.GameMode.xml', + 'org.freedesktop.portal.GlobalShortcuts.xml', + 'org.freedesktop.portal.Inhibit.xml', + 'org.freedesktop.portal.InputCapture.xml', + 'org.freedesktop.portal.Location.xml', + 'org.freedesktop.portal.MemoryMonitor.xml', + 'org.freedesktop.portal.NetworkMonitor.xml', + 'org.freedesktop.portal.Notification.xml', + 'org.freedesktop.portal.OpenURI.xml', + 'org.freedesktop.portal.PowerProfileMonitor.xml', + 'org.freedesktop.portal.Print.xml', + 'org.freedesktop.portal.ProxyResolver.xml', + 'org.freedesktop.portal.Realtime.xml', + 'org.freedesktop.portal.RemoteDesktop.xml', + 'org.freedesktop.portal.Request.xml', + 'org.freedesktop.portal.ScreenCast.xml', + 'org.freedesktop.portal.Screenshot.xml', + 'org.freedesktop.portal.Secret.xml', + 'org.freedesktop.portal.Session.xml', + 'org.freedesktop.portal.Settings.xml', + 'org.freedesktop.portal.Trash.xml', + 'org.freedesktop.portal.Wallpaper.xml', +) + +portal_impl_sources = files( + 'org.freedesktop.impl.portal.Access.xml', + 'org.freedesktop.impl.portal.Account.xml', + 'org.freedesktop.impl.portal.AppChooser.xml', + 'org.freedesktop.impl.portal.Background.xml', + 'org.freedesktop.impl.portal.Clipboard.xml', + 'org.freedesktop.impl.portal.DynamicLauncher.xml', + 'org.freedesktop.impl.portal.Email.xml', + 'org.freedesktop.impl.portal.FileChooser.xml', + 'org.freedesktop.impl.portal.GlobalShortcuts.xml', + 'org.freedesktop.impl.portal.Inhibit.xml', + 'org.freedesktop.impl.portal.InputCapture.xml', + 'org.freedesktop.impl.portal.Lockdown.xml', + 'org.freedesktop.impl.portal.Notification.xml', + 'org.freedesktop.impl.portal.PermissionStore.xml', + 'org.freedesktop.impl.portal.Print.xml', + 'org.freedesktop.impl.portal.RemoteDesktop.xml', + 'org.freedesktop.impl.portal.Request.xml', + 'org.freedesktop.impl.portal.ScreenCast.xml', + 'org.freedesktop.impl.portal.Screenshot.xml', + 'org.freedesktop.impl.portal.Secret.xml', + 'org.freedesktop.impl.portal.Session.xml', + 'org.freedesktop.impl.portal.Settings.xml', + 'org.freedesktop.impl.portal.Wallpaper.xml', +) + +background_monitor_sources = files( + 'org.freedesktop.background.Monitor.xml', +) + +install_data([portal_sources, portal_impl_sources], + install_dir: datadir / 'dbus-1' / 'interfaces', +) diff --git a/data/org.freedesktop.background.Monitor.xml b/data/org.freedesktop.background.Monitor.xml new file mode 100644 index 0000000..1a2028a --- /dev/null +++ b/data/org.freedesktop.background.Monitor.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + diff --git a/data/org.freedesktop.impl.portal.Access.xml b/data/org.freedesktop.impl.portal.Access.xml new file mode 100644 index 0000000..e20f846 --- /dev/null +++ b/data/org.freedesktop.impl.portal.Access.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.impl.portal.Account.xml b/data/org.freedesktop.impl.portal.Account.xml new file mode 100644 index 0000000..a6acb33 --- /dev/null +++ b/data/org.freedesktop.impl.portal.Account.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.impl.portal.AppChooser.xml b/data/org.freedesktop.impl.portal.AppChooser.xml new file mode 100644 index 0000000..cbb42ee --- /dev/null +++ b/data/org.freedesktop.impl.portal.AppChooser.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.impl.portal.Background.xml b/data/org.freedesktop.impl.portal.Background.xml new file mode 100644 index 0000000..509c516 --- /dev/null +++ b/data/org.freedesktop.impl.portal.Background.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.impl.portal.Clipboard.xml b/data/org.freedesktop.impl.portal.Clipboard.xml new file mode 100644 index 0000000..66e5dcb --- /dev/null +++ b/data/org.freedesktop.impl.portal.Clipboard.xml @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.impl.portal.DynamicLauncher.xml b/data/org.freedesktop.impl.portal.DynamicLauncher.xml new file mode 100644 index 0000000..5a3fec9 --- /dev/null +++ b/data/org.freedesktop.impl.portal.DynamicLauncher.xml @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.impl.portal.Email.xml b/data/org.freedesktop.impl.portal.Email.xml new file mode 100644 index 0000000..4fdd240 --- /dev/null +++ b/data/org.freedesktop.impl.portal.Email.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.impl.portal.FileChooser.xml b/data/org.freedesktop.impl.portal.FileChooser.xml new file mode 100644 index 0000000..b3bf981 --- /dev/null +++ b/data/org.freedesktop.impl.portal.FileChooser.xml @@ -0,0 +1,346 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.impl.portal.GlobalShortcuts.xml b/data/org.freedesktop.impl.portal.GlobalShortcuts.xml new file mode 100644 index 0000000..01018e9 --- /dev/null +++ b/data/org.freedesktop.impl.portal.GlobalShortcuts.xml @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.impl.portal.Inhibit.xml b/data/org.freedesktop.impl.portal.Inhibit.xml new file mode 100644 index 0000000..0c20d76 --- /dev/null +++ b/data/org.freedesktop.impl.portal.Inhibit.xml @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.impl.portal.InputCapture.xml b/data/org.freedesktop.impl.portal.InputCapture.xml new file mode 100644 index 0000000..c431a41 --- /dev/null +++ b/data/org.freedesktop.impl.portal.InputCapture.xml @@ -0,0 +1,436 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.impl.portal.Lockdown.xml b/data/org.freedesktop.impl.portal.Lockdown.xml new file mode 100644 index 0000000..023e858 --- /dev/null +++ b/data/org.freedesktop.impl.portal.Lockdown.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.impl.portal.Notification.xml b/data/org.freedesktop.impl.portal.Notification.xml new file mode 100644 index 0000000..39e4fa8 --- /dev/null +++ b/data/org.freedesktop.impl.portal.Notification.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.impl.portal.PermissionStore.xml b/data/org.freedesktop.impl.portal.PermissionStore.xml new file mode 100644 index 0000000..36d98ae --- /dev/null +++ b/data/org.freedesktop.impl.portal.PermissionStore.xml @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.impl.portal.Print.xml b/data/org.freedesktop.impl.portal.Print.xml new file mode 100644 index 0000000..88e6163 --- /dev/null +++ b/data/org.freedesktop.impl.portal.Print.xml @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.impl.portal.RemoteDesktop.xml b/data/org.freedesktop.impl.portal.RemoteDesktop.xml new file mode 100644 index 0000000..a8dc551 --- /dev/null +++ b/data/org.freedesktop.impl.portal.RemoteDesktop.xml @@ -0,0 +1,467 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.impl.portal.Request.xml b/data/org.freedesktop.impl.portal.Request.xml new file mode 100644 index 0000000..4402c40 --- /dev/null +++ b/data/org.freedesktop.impl.portal.Request.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + diff --git a/data/org.freedesktop.impl.portal.ScreenCast.xml b/data/org.freedesktop.impl.portal.ScreenCast.xml new file mode 100644 index 0000000..e0d6c21 --- /dev/null +++ b/data/org.freedesktop.impl.portal.ScreenCast.xml @@ -0,0 +1,278 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.impl.portal.Screenshot.xml b/data/org.freedesktop.impl.portal.Screenshot.xml new file mode 100644 index 0000000..1c27651 --- /dev/null +++ b/data/org.freedesktop.impl.portal.Screenshot.xml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.impl.portal.Secret.xml b/data/org.freedesktop.impl.portal.Secret.xml new file mode 100644 index 0000000..f540a72 --- /dev/null +++ b/data/org.freedesktop.impl.portal.Secret.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.impl.portal.Session.xml b/data/org.freedesktop.impl.portal.Session.xml new file mode 100644 index 0000000..a2e7a2a --- /dev/null +++ b/data/org.freedesktop.impl.portal.Session.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.impl.portal.Settings.xml b/data/org.freedesktop.impl.portal.Settings.xml new file mode 100644 index 0000000..7659732 --- /dev/null +++ b/data/org.freedesktop.impl.portal.Settings.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.impl.portal.Wallpaper.xml b/data/org.freedesktop.impl.portal.Wallpaper.xml new file mode 100644 index 0000000..c16722a --- /dev/null +++ b/data/org.freedesktop.impl.portal.Wallpaper.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.Account.xml b/data/org.freedesktop.portal.Account.xml new file mode 100644 index 0000000..c8fb98a --- /dev/null +++ b/data/org.freedesktop.portal.Account.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.Background.xml b/data/org.freedesktop.portal.Background.xml new file mode 100644 index 0000000..8ef85dc --- /dev/null +++ b/data/org.freedesktop.portal.Background.xml @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.Camera.xml b/data/org.freedesktop.portal.Camera.xml new file mode 100644 index 0000000..f655265 --- /dev/null +++ b/data/org.freedesktop.portal.Camera.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.Clipboard.xml b/data/org.freedesktop.portal.Clipboard.xml new file mode 100644 index 0000000..912341c --- /dev/null +++ b/data/org.freedesktop.portal.Clipboard.xml @@ -0,0 +1,182 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.Device.xml b/data/org.freedesktop.portal.Device.xml new file mode 100644 index 0000000..3caf194 --- /dev/null +++ b/data/org.freedesktop.portal.Device.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.Documents.xml b/data/org.freedesktop.portal.Documents.xml new file mode 100644 index 0000000..c5be1eb --- /dev/null +++ b/data/org.freedesktop.portal.Documents.xml @@ -0,0 +1,279 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.DynamicLauncher.xml b/data/org.freedesktop.portal.DynamicLauncher.xml new file mode 100644 index 0000000..a6ce2cb --- /dev/null +++ b/data/org.freedesktop.portal.DynamicLauncher.xml @@ -0,0 +1,329 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.Email.xml b/data/org.freedesktop.portal.Email.xml new file mode 100644 index 0000000..39959e3 --- /dev/null +++ b/data/org.freedesktop.portal.Email.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.FileChooser.xml b/data/org.freedesktop.portal.FileChooser.xml new file mode 100644 index 0000000..6d167e9 --- /dev/null +++ b/data/org.freedesktop.portal.FileChooser.xml @@ -0,0 +1,430 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.FileTransfer.xml b/data/org.freedesktop.portal.FileTransfer.xml new file mode 100644 index 0000000..f789578 --- /dev/null +++ b/data/org.freedesktop.portal.FileTransfer.xml @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.GameMode.xml b/data/org.freedesktop.portal.GameMode.xml new file mode 100644 index 0000000..c4c8878 --- /dev/null +++ b/data/org.freedesktop.portal.GameMode.xml @@ -0,0 +1,220 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.GlobalShortcuts.xml b/data/org.freedesktop.portal.GlobalShortcuts.xml new file mode 100644 index 0000000..1fc2b0a --- /dev/null +++ b/data/org.freedesktop.portal.GlobalShortcuts.xml @@ -0,0 +1,268 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.Inhibit.xml b/data/org.freedesktop.portal.Inhibit.xml new file mode 100644 index 0000000..e91bd22 --- /dev/null +++ b/data/org.freedesktop.portal.Inhibit.xml @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.InputCapture.xml b/data/org.freedesktop.portal.InputCapture.xml new file mode 100644 index 0000000..7f3b3a5 --- /dev/null +++ b/data/org.freedesktop.portal.InputCapture.xml @@ -0,0 +1,587 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.Location.xml b/data/org.freedesktop.portal.Location.xml new file mode 100644 index 0000000..35b40f3 --- /dev/null +++ b/data/org.freedesktop.portal.Location.xml @@ -0,0 +1,168 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.MemoryMonitor.xml b/data/org.freedesktop.portal.MemoryMonitor.xml new file mode 100644 index 0000000..c347581 --- /dev/null +++ b/data/org.freedesktop.portal.MemoryMonitor.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.NetworkMonitor.xml b/data/org.freedesktop.portal.NetworkMonitor.xml new file mode 100644 index 0000000..5359b0c --- /dev/null +++ b/data/org.freedesktop.portal.NetworkMonitor.xml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.Notification.xml b/data/org.freedesktop.portal.Notification.xml new file mode 100644 index 0000000..5d91fb9 --- /dev/null +++ b/data/org.freedesktop.portal.Notification.xml @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.OpenURI.xml b/data/org.freedesktop.portal.OpenURI.xml new file mode 100644 index 0000000..0c42029 --- /dev/null +++ b/data/org.freedesktop.portal.OpenURI.xml @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.PowerProfileMonitor.xml b/data/org.freedesktop.portal.PowerProfileMonitor.xml new file mode 100644 index 0000000..daf487a --- /dev/null +++ b/data/org.freedesktop.portal.PowerProfileMonitor.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.Print.xml b/data/org.freedesktop.portal.Print.xml new file mode 100644 index 0000000..8d0587e --- /dev/null +++ b/data/org.freedesktop.portal.Print.xml @@ -0,0 +1,396 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.ProxyResolver.xml b/data/org.freedesktop.portal.ProxyResolver.xml new file mode 100644 index 0000000..4b39fc0 --- /dev/null +++ b/data/org.freedesktop.portal.ProxyResolver.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.Realtime.xml b/data/org.freedesktop.portal.Realtime.xml new file mode 100644 index 0000000..1408291 --- /dev/null +++ b/data/org.freedesktop.portal.Realtime.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.RemoteDesktop.xml b/data/org.freedesktop.portal.RemoteDesktop.xml new file mode 100644 index 0000000..36dc497 --- /dev/null +++ b/data/org.freedesktop.portal.RemoteDesktop.xml @@ -0,0 +1,489 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.Request.xml b/data/org.freedesktop.portal.Request.xml new file mode 100644 index 0000000..c1abb4e --- /dev/null +++ b/data/org.freedesktop.portal.Request.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.ScreenCast.xml b/data/org.freedesktop.portal.ScreenCast.xml new file mode 100644 index 0000000..3fc1d4e --- /dev/null +++ b/data/org.freedesktop.portal.ScreenCast.xml @@ -0,0 +1,324 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.Screenshot.xml b/data/org.freedesktop.portal.Screenshot.xml new file mode 100644 index 0000000..f8927de --- /dev/null +++ b/data/org.freedesktop.portal.Screenshot.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.Secret.xml b/data/org.freedesktop.portal.Secret.xml new file mode 100644 index 0000000..a647baf --- /dev/null +++ b/data/org.freedesktop.portal.Secret.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.Session.xml b/data/org.freedesktop.portal.Session.xml new file mode 100644 index 0000000..8a902b2 --- /dev/null +++ b/data/org.freedesktop.portal.Session.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.Settings.xml b/data/org.freedesktop.portal.Settings.xml new file mode 100644 index 0000000..669997a --- /dev/null +++ b/data/org.freedesktop.portal.Settings.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.Trash.xml b/data/org.freedesktop.portal.Trash.xml new file mode 100644 index 0000000..cacbfd4 --- /dev/null +++ b/data/org.freedesktop.portal.Trash.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.Wallpaper.xml b/data/org.freedesktop.portal.Wallpaper.xml new file mode 100644 index 0000000..a214690 --- /dev/null +++ b/data/org.freedesktop.portal.Wallpaper.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/docbook.css b/doc/docbook.css new file mode 100644 index 0000000..ddb9391 --- /dev/null +++ b/doc/docbook.css @@ -0,0 +1,95 @@ +body { + font-family: sans-serif; + max-width: 850px; + margin: auto; + overflow-wrap: break-word; +} +h1.title { +} +.permission { + color: #ee0000; + text-decoration: underline; +} +.synopsis, +.classsynopsis { + background: #eeeeee; + border: solid 1px #aaaaaa; + padding: 0.5em; +} +.programlisting { + background: #eeeeff; + border: solid 1px #aaaaff; + padding: 0.5em; + overflow: scroll; +} +.variablelist { + padding: 4px; + margin-left: 1em; +} +.variablelist td:first-child { + vertical-align: top; +} +td.shortcuts { + color: #770000; + font-size: 80%; +} +div.refnamediv { + margin-top: 2em; +} +div.toc { + border: 2em; +} +a { + text-decoration: none; +} +a:hover { + text-decoration: underline; + color: #ff0000; +} + +div.table table { + border-collapse: collapse; + border-spacing: 0px; + border-style: solid; + border-color: #777777; + border-width: 1px; +} + +div.table table td, +div.table table th { + border-style: solid; + border-color: #777777; + border-width: 1px; + padding: 3px; + vertical-align: top; +} + +div.table table th { + background-color: #eeeeee; +} + +@media (prefers-color-scheme: dark) { + html { + background-color: #222222; + color: #dddddd; + } + + a { + color: #8886fa; + } + + a:hover { + color: #5a57fa; + } + + .synopsis, + .classsynopsis { + background: #161616; + border: solid 1px #aaaaaa; + overflow: scroll; + } + .programlisting { + background: #161616; + border: solid 1px #aaaaff; + } +} diff --git a/doc/meson.build b/doc/meson.build new file mode 100644 index 0000000..23ba8ec --- /dev/null +++ b/doc/meson.build @@ -0,0 +1,66 @@ +if build_docbook + xslfile = files('xmlto-config.xsl') + + portal_docs_xml = configure_file( + input: 'portal-docs.xml.in', + output: '@BASENAME@', + configuration: { + 'VERSION': meson.project_version(), + }, + ) + + docs = custom_target( + 'html', + build_by_default: true, + depends: [portal_built_sources, impl_built_sources], + input: portal_docs_xml, + output: 'portal-docs.html', + command: [xmlto, get_option('xmlto-flags'), 'xhtml-nochunks', + '-o', '@OUTDIR@', + '-m', xslfile, + '--searchpath', meson.project_build_root() / 'src', + '@INPUT@'], + install: true, + install_dir: docs_dir, + ) + doc_extra = files( + 'docbook.css', + 'redirect.html' + ) + foreach f : doc_extra + configure_file( + input: f, + output: '@PLAINNAME@', + copy: true, + install: true, + install_dir: docs_dir, + ) + endforeach + + rst2man = find_program('rst2man', 'rst2man.py', required: false) + if rst2man.found() + rst2man_flags = [ + '--syntax-highlight=none', + ] + + man_pages = [ + { 'input': 'portals-conf.rst', 'output': 'portals.conf', 'section': '5' }, + ] + + foreach man_page: man_pages + man_input = man_page.get('input') + man_output = man_page.get('output', man_input) + man_section = man_page.get('section', '1') + man_full = '@0@.@1@'.format(man_output, man_section) + + custom_target('man-' + man_output, + input: man_input, + output: man_full, + command: [ rst2man, rst2man_flags, '@INPUT@' ], + capture: true, + install: true, + install_dir: get_option('mandir') / 'man@0@'.format(man_section), + ) + endforeach + endif +endif diff --git a/doc/portal-docs.xml.in b/doc/portal-docs.xml.in new file mode 100644 index 0000000..c32a8e0 --- /dev/null +++ b/doc/portal-docs.xml.in @@ -0,0 +1,188 @@ + + +]> + + + Portal Documentation + Version @VERSION@ + + + Common Conventions + + Requests made via portal interfaces generally involve user interaction, + and dialogs that can stay open for a long time. Therefore portal APIs + don't just use async method calls (which time out after at most 25 seconds), + but instead return results via a Response signal on Request objects. + + + Portal APIs don't use properties very much. This is partially because + we need to be careful about the flow of information, and partially because + it would be unexpected to have a dialog appear when you just set a property. + +
+ Portal requests + + The general flow of the portal API is that the application makes + a portal request, the portal replies to that method call with a + handle (i.e. object path) to Request object that corresponds to the + request. The object is exported on the bus and stays alive as long + as the user interaction lasts. When the user interaction is over, + the portal sends a Response signal back to the application with + the results from the interaction, if any. + + + To avoid a race condition between the caller subscribing to the signal + after receiving the reply for the method call and the signal getting emitted, + a convention for Request object paths has been established that allows the + caller to subscribe to the signal before making the method call. + +
+
+ Sessions + + Some portal requests are connected to each other and need to be used in + sequence. The pattern we use in such cases is a Session object. Just like + Requests, Sessions are represented by an object path, that is returned by + the initial CreateSession call. Subsequent calls take the object path of + the session they operate on as an argument. + + + Sessions can be ended from the application side by calling the Close() + method. They can also be closed from the service side, in which case the + ::Closed signal is emitted to inform the application. + +
+
+ Parent window identifiers + + Most portals interact with the user by showing dialogs. These dialogs + should generally be placed on top of the application window that triggered + them. To arrange this, the compositor needs to know about the application + window. Many portal requests expect a "parent_window" string argument for + this reason. + + + Under X11, the "parent_window" argument should have the form + "x11:XID", where XID + is the XID of the application window in hexadecimal notation. + + + Under Wayland, it should have the form "wayland:HANDLE", + where HANDLE is a surface handle obtained with the + xdg_foreign protocol. + + + For other windowing systems, or if you don't have a suitable handle, just + pass an empty string for "parent_window". + +
+
+ + Portal API Reference + + + Portal interfaces are available to sandboxed applications with the + default filtered session bus access of Flatpak. Unless otherwise + specified, they appear under the bus name org.freedesktop.portal.Desktop + and the object path /org/freedesktop/portal/desktop on the session + bus. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Portal Backend API Reference + + + The backend interfaces are used by the portal frontend to + carry out portal requests. They are provided by a separate process + (or processes), and are not accessible to sandboxed applications. + + + The separation or the portal infrastructure into frontend and backend + is a clean way to provide suitable user interfaces that fit into + different desktop environments, while sharing the portal frontend. + + + The portal backends are focused on providing user interfaces and + accessing session- or host-specific APIs and resources. Details of + interacting with the containment infrastructure such as checking + access, registering files in the document portal, etc., are handled + by the portal frontend. + + + + + + + + + + + + + + + + + + + + + + + + + + + Background Apps Monitoring API + + + In addition to managing the regular interfaces that sandboxed + applications use to interfact with the host system, xdg-desktop-portals + also monitors running applications without an active window - if the + portal backend provides an implementation of the Background portal. + + + This API can be used by host system services to provide rich interfaces + to manage background running applications. + + + + +
diff --git a/doc/portals-conf.rst b/doc/portals-conf.rst new file mode 100644 index 0000000..c3ad8ee --- /dev/null +++ b/doc/portals-conf.rst @@ -0,0 +1,87 @@ +.. _portals.conf(5): + +============ +portals.conf +============ + +-------------------------------- +XDG desktop portal configuration +-------------------------------- + +DESCRIPTION +----------- + +xdg-desktop-portal uses a configuration file to determine which portal backend +should be used to provide the implementation for the requested interface. + +The configuration file can be found in the following locations: + +- ``/etc/xdg-desktop-portal/portals.conf``, for site-wide configuration + +- ``$XDG_CONFIG_HOME/xdg-desktop-portal/portals.conf``, for user-specific + configuration + +Additionally, every desktop environment can provide a portal configuration file +named ``DESKTOP-portals.conf``, where ``DESKTOP`` is the lowercase name also +used in the ``XDG_CURRENT_DESKTOP`` environment variable. + +FILE FORMAT +----------- + +The format of the portals configuration file is the same ``.ini`` format used by +systemd unit files or application desktop files. + +``[preferred]`` + + The main configuration group for preferred portals. + +The following keys can be present in the ``preferred`` group: + +``default`` *(string)* + + The default portal backend to use for every interface, unless the interface + is listed explicitly. + +``org.freedesktop.impl.portal.*`` *(string)* + + One of the valid portal interface implementations exposed by + xdg-desktop-portal. + +Each key in the group contains a semi-colon separated list of portal backend +implementation, to be searched for an implementation of the requested interface, +in the same order as specified in the configuration file. Additionally, the +special values ``none`` and ``*`` can be used: + +``none`` + + Do not provide a portal implementation for this interface. + +``*`` + + Use the first portal implementation found, in lexicographical order. + +EXAMPLE +------- + +:: + + [preferred] + # Use xdg-desktop-portal-gtk for every portal interface... + default=gtk + # ... except for the Screencast interface + org.freedesktop.impl.portal.Screencast=gnome + + +ENVIRONMENT +----------- + +``XDG_CONFIG_HOME`` + + The per-user ``portals.conf`` file is located in this directory. The default + is ``$HOME/.config`` + + +SEE ALSO +-------- + +- `XDG Desktop Entry specification `_ diff --git a/doc/redirect.html b/doc/redirect.html new file mode 100644 index 0000000..92c6a08 --- /dev/null +++ b/doc/redirect.html @@ -0,0 +1,19 @@ + + + + + Redirect to the new docs location + + + + + +

+ If your browser supports Refresh, + you'll be redirected to the new xdg-desktop-portal documentations. +

+ + + + diff --git a/doc/xmlto-config.xsl b/doc/xmlto-config.xsl new file mode 100644 index 0000000..b07ef51 --- /dev/null +++ b/doc/xmlto-config.xsl @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/document-portal/document-enums.h b/document-portal/document-enums.h new file mode 100644 index 0000000..299b780 --- /dev/null +++ b/document-portal/document-enums.h @@ -0,0 +1,23 @@ +#pragma once + +G_BEGIN_DECLS + +typedef enum { + DOCUMENT_PERMISSION_FLAGS_READ = (1 << 0), + DOCUMENT_PERMISSION_FLAGS_WRITE = (1 << 1), + DOCUMENT_PERMISSION_FLAGS_GRANT_PERMISSIONS = (1 << 2), + DOCUMENT_PERMISSION_FLAGS_DELETE = (1 << 3), + + DOCUMENT_PERMISSION_FLAGS_ALL = ((1 << 4) - 1) +} DocumentPermissionFlags; + +typedef enum { + DOCUMENT_ADD_FLAGS_REUSE_EXISTING = (1 << 0), + DOCUMENT_ADD_FLAGS_PERSISTENT = (1 << 1), + DOCUMENT_ADD_FLAGS_AS_NEEDED_BY_APP = (1 << 2), + DOCUMENT_ADD_FLAGS_DIRECTORY = (1 << 3), + + DOCUMENT_ADD_FLAGS_FLAGS_ALL = ((1 << 4) - 1) +} DocumentAddFullFlags; + +G_END_DECLS diff --git a/document-portal/document-portal-fuse.c b/document-portal/document-portal-fuse.c new file mode 100644 index 0000000..6209592 --- /dev/null +++ b/document-portal/document-portal-fuse.c @@ -0,0 +1,3554 @@ +#include "config.h" + +#define FUSE_USE_VERSION 35 + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef HAVE_SYS_STATFS_H +#include +#endif +#include +#ifdef HAVE_SYS_MOUNT_H +#include +#endif +#ifdef HAVE_SYS_XATTR_H +#include +#endif +#ifdef HAVE_SYS_EXTATTR_H +#include +#endif +#include +#include + +#include "document-portal-fuse.h" +#include "document-store.h" +#include "src/xdp-utils.h" + +#ifndef O_FSYNC +#define O_FSYNC O_SYNC +#endif + +#ifndef ENODATA +#define ENODATA ENOATTR +#endif + +/* Inode ownership model + * + * The document portal exposes something as a filesystem that it + * doesn't have full control over. For instance at any point some + * other process can rename an exposed file on the real filesystem and + * we won't be told about this. This means that in general we always + * return 0 for the cacheable lifetimes of entries and file attributes. + * (Except for the virtual directories we have full control of, the + * below only discusses real files). + * + * However, even though we don't have full control of the underlying + * filesystem the *kernel* has. This means we can used that to get + * the correct semantics. + * + * For example, assume that a directory is held opened by a process + * (for example, it could be the CWD of the process). When we opened + * the directory via a LOOKUP operation we returned an inode to it, + * and for as long as the kernel has this inode around (i.e. until it + * sent a FORGET message) it can send operations on this inode without + * looking it up again. For example if the above process used a + * relative path. + * + * Now, consider the case where the app chdir():ed into the fuse + * directory, but after that the backing directory was renamed ouside + * the fuse filesystem. The fuse inode representation for the inode + * cannot be the directory name, because the expected semantics is + * that further relative pathnames from the app will still resolve + * to the same directory independent of its location in the tree. + * + * The way we do this is to keep a O_PATH file descriptor around for + * each underlying inode. This is represented by the XdpPhysicalInode + * type and we have a hashtable from backing dev+inode to a these + * so that we can use one fd per backing inode even when the file + * is visible in many places. + * + * Since we don't do any caching, each LOOKUP operation will do a + * statat() on the underlying filesystem. However, we then use the + * result of that to lookup (via backing dev+ino) the previous inode + * (as long as it still lives) if the backing file was unchanged. + * + * One problem with this approach is that the kernel tends to keep + * inodes alive for a very long time even if they are *only* + * references by the dcache (event though we will not really use the + * dcache info due to the 0 valid time). This is unfortunate, because + * it means we will keep a lot of file descriptor open. But, we + * can't know if the kernel needs the inode for some non-dcache use + * so we can't close the file descriptors. + * + * To work around this we regularly emit entry invalidation calls + * to the kernel, which will make it forget the inodes that are + * only pinned by the dcache. + */ + + +#define NON_DOC_DIR_PERMS 0500 +#define DOC_DIR_PERMS_FILE 0700 +#define DOC_DIR_PERMS_DIR 0500 + +static GThread *fuse_thread = NULL; +static struct fuse_session *session = NULL; +G_LOCK_DEFINE (session); +static char *mount_path = NULL; +static pthread_t fuse_pthread = 0; +static uid_t my_uid; +static gid_t my_gid; + +/* from libfuse */ +#define FUSE_UNKNOWN_INO 0xffffffff + +#define BY_APP_NAME "by-app" + +typedef struct { + ino_t ino; + dev_t dev; +} DevIno; + +typedef enum { + XDP_DOMAIN_ROOT, + XDP_DOMAIN_BY_APP, + XDP_DOMAIN_APP, + XDP_DOMAIN_DOCUMENT, +} XdpDomainType; + +typedef struct _XdpDomain XdpDomain; +typedef struct _XdpInode XdpInode; + +struct _XdpDomain { + gint ref_count; /* atomic */ + XdpDomainType type; + + XdpDomain *parent; + XdpInode *parent_inode; /* Inode of the parent domain (NULL for root) */ + + char *doc_id; /* NULL for root, by-app, app */ + char *app_id; /* NULL for root, by-app, non-app id */ + + /* root: by docid + * app: by docid + * by_app: by app + * document: by physical + */ + GHashTable *inodes; /* Protected by domain_inodes */ + + /* Below only used for XDP_DOMAIN_DOCUMENT */ + + char *doc_path; /* path to the directory the files are in */ + char *doc_file; /* != NULL for non-directory documents */ + guint64 doc_dir_device; + guint64 doc_dir_inode; + guint32 doc_flags; + + int doc_queued_invalidate; /* Access atomically, 1 if queued invalidate */ + + /* Below is mutable, protected by mutex */ + GMutex tempfile_mutex; + GHashTable *tempfiles; /* Name -> physical */ +}; + +static void xdp_domain_unref (XdpDomain *domain); +G_DEFINE_AUTOPTR_CLEANUP_FUNC (XdpDomain, xdp_domain_unref) + +G_LOCK_DEFINE (domain_inodes); + +typedef struct { + gint ref_count; /* atomic */ + DevIno backing_devino; + int fd; /* O_PATH fd */ +} XdpPhysicalInode; + +static XdpPhysicalInode *xdp_physical_inode_ref (XdpPhysicalInode *inode); +static void xdp_physical_inode_unref (XdpPhysicalInode *inode); +G_DEFINE_AUTOPTR_CLEANUP_FUNC (XdpPhysicalInode, xdp_physical_inode_unref) + +typedef struct { + gint ref_count; /* atomic */ + + char *name; /* This changes over time (i.e. in renames) + protected by domain->tempfile_mutex, + used as key in domain->tempfiles */ + char *tempname; /* Real filename on disk. + This can be NULLed to avoid unlink at finalize */ + XdpInode *inode; +} XdpTempfile; + +static XdpTempfile *xdp_tempfile_ref (XdpTempfile *tempfile); +static void xdp_tempfile_unref (XdpTempfile *tempfile); +G_DEFINE_AUTOPTR_CLEANUP_FUNC (XdpTempfile, xdp_tempfile_unref) + +struct _XdpInode { + guint64 ino; + gint ref_count; /* atomic, includes one ref if kernel_ref_count != 0 */ + gint kernel_ref_count; /* atomic */ + + XdpDomain *domain; + + /* The below are only used for XDP_DOMAIN_DOCUMENT inodes */ + XdpPhysicalInode *physical; + /* The root of the domain, or NULL for the domain. We use this to + * keep the root document inode alive so that when the kernel + * forgets it and then looks it up we will not get a new inode and + * thus a new domain. */ + XdpInode *domain_root_inode; +}; + +typedef struct { + int fd; +} XdpFile; + + +typedef struct { + DIR *dir; + struct dirent *entry; + off_t offset; + + char *dirbuf; + gsize dirbuf_size; +} XdpDir; + +XdpInode *root_inode; +XdpInode *by_app_inode; + +static XdpInode *xdp_inode_ref (XdpInode *inode); +static void xdp_inode_unref (XdpInode *inode); + +/* Lookup by inode for verification */ +static GHashTable *all_inodes; /* guint64 -> XdpInode */ +static guint64 next_virtual_inode = FUSE_ROOT_ID; /* root is the first inode created, so it gets this */ +G_LOCK_DEFINE (all_inodes); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (XdpInode, xdp_inode_unref) + +static int ensure_docdir_inode (XdpInode *parent, + int o_path_fd_in, /* Takes ownership */ + struct fuse_entry_param *e, + XdpInode **inode_out); + +static gboolean +app_can_write_doc (PermissionDbEntry *entry, const char *app_id) +{ + if (app_id == NULL) + return TRUE; + + if (document_entry_has_permissions_by_app_id (entry, app_id, DOCUMENT_PERMISSION_FLAGS_WRITE)) + return TRUE; + + return FALSE; +} + +static gboolean +app_can_see_doc (PermissionDbEntry *entry, const char *app_id) +{ + if (app_id == NULL) + return TRUE; + + if (document_entry_has_permissions_by_app_id (entry, app_id, DOCUMENT_PERMISSION_FLAGS_READ)) + return TRUE; + + return FALSE; +} + +static char * +fd_to_path (int fd) +{ + return g_strdup_printf ("/proc/self/fd/%d", fd); +} + +static char * +open_flags_to_string (int flags) +{ + GString *s; + const char *mode; + + switch (flags & O_ACCMODE) + { + case O_RDONLY: + mode = "RDONLY"; + break; + case O_WRONLY: + mode = "WRONLY"; + break; + case O_RDWR: + default: + mode = "RDWR"; + break; + } + + s = g_string_new (mode); + + if (flags & O_NONBLOCK) + g_string_append (s, ",NONBLOCK"); + if (flags & O_APPEND) + g_string_append (s, ",APPEND"); + if (flags & O_SYNC) + g_string_append (s, ",SYNC"); + if (flags & O_ASYNC) + g_string_append (s, ",ASYNC"); + if (flags & O_FSYNC) + g_string_append (s, ",FSYNC"); +#ifdef O_DSYNC + if (flags & O_DSYNC) + g_string_append (s, ",DSYNC"); +#endif + if (flags & O_CREAT) + g_string_append (s, ",CREAT"); + if (flags & O_TRUNC) + g_string_append (s, ",TRUNC"); + if (flags & O_EXCL) + g_string_append (s, ",EXCL"); + if (flags & O_CLOEXEC) + g_string_append (s, ",CLOEXEC"); + if (flags & O_DIRECT) + g_string_append (s, ",DIRECT"); +#ifdef O_LARGEFILE + if (flags & O_LARGEFILE) + g_string_append (s, ",LARGEFILE"); +#endif +#ifdef O_NOATIME + if (flags & O_NOATIME) + g_string_append (s, ",NOATIME"); +#endif + if (flags & O_NOCTTY) + g_string_append (s, ",NOCTTY"); + if (flags & O_PATH) + g_string_append (s, ",PATH"); +#ifdef O_TMPFILE + if (flags & O_TMPFILE) + g_string_append (s, ",TMPFILE"); +#endif + + return g_string_free (s, FALSE); +} + + +static char * +setattr_flags_to_string (int flags) +{ + GString *s = g_string_new (""); + + if (flags & FUSE_SET_ATTR_MODE) + g_string_append (s, "MODE,"); + + if (flags & FUSE_SET_ATTR_UID) + g_string_append (s, "UID,"); + + if (flags & FUSE_SET_ATTR_GID) + g_string_append (s, "GID,"); + + if (flags & FUSE_SET_ATTR_SIZE) + g_string_append (s, "SIZE,"); + + if (flags & FUSE_SET_ATTR_ATIME) + g_string_append (s, "ATIME,"); + + if (flags & FUSE_SET_ATTR_MTIME) + g_string_append (s, "MTIME,"); + + if (flags & FUSE_SET_ATTR_ATIME_NOW) + g_string_append (s, "ATIME_NOW,"); + + if (flags & FUSE_SET_ATTR_MTIME_NOW) + g_string_append (s, "MTIME_NOW,"); + + /* Remove last comma */ + if (s->len > 0) + g_string_truncate (s, s->len - 1); + + return g_string_free (s, FALSE); +} + +static char * +renameat2_flags_to_string (int flags) +{ +#if HAVE_RENAMEAT2 + GString *s = g_string_new (""); + + if (flags & RENAME_EXCHANGE) + g_string_append (s, "EXCHANGE,"); + + if (flags & RENAME_NOREPLACE) + g_string_append (s, "NOREPLACE,"); + + if (flags & RENAME_WHITEOUT) + g_string_append (s, "WHITEOUT,"); + + /* Remove last comma */ + if (s->len > 0) + g_string_truncate (s, s->len - 1); + + return g_string_free (s, FALSE); +#else + return g_strdup_printf ("%#x", flags); +#endif +} + +static guint +devino_hash (gconstpointer key) +{ + DevIno *devino = (DevIno *)key; + + return (devino->ino >> 2) ^ devino->dev; +} + +static gboolean +devino_equal (gconstpointer _a, + gconstpointer _b) +{ + DevIno *a = (DevIno *)_a; + DevIno *b = (DevIno *)_b; + return a->ino == b->ino && a->dev == b->dev; +} + +/* Lookup by physical backing devino */ +static GHashTable *physical_inodes; +G_LOCK_DEFINE (physical_inodes); + + +/* Takes ownership of the o_path fd if passed in */ +static XdpPhysicalInode * +ensure_physical_inode (dev_t dev, ino_t ino, int o_path_fd) +{ + DevIno devino = {ino, dev}; + XdpPhysicalInode *inode = NULL; + + G_LOCK (physical_inodes); + + inode = g_hash_table_lookup (physical_inodes, &devino); + if (inode != NULL) + { + inode = xdp_physical_inode_ref (inode); + close (o_path_fd); + } + else + { + /* Takes ownership of fd */ + inode = g_new0 (XdpPhysicalInode, 1); + inode->ref_count = 1; + inode->fd = o_path_fd; + inode->backing_devino = devino; + g_hash_table_insert (physical_inodes, &inode->backing_devino, inode); + } + + G_UNLOCK (physical_inodes); + + return inode; +} + +static XdpPhysicalInode * +xdp_physical_inode_ref (XdpPhysicalInode *inode) +{ + g_atomic_int_inc (&inode->ref_count); + return inode; +} + +static void +xdp_physical_inode_unref (XdpPhysicalInode *inode) +{ + gint old_ref; + + /* here we want to atomically do: if (ref_count>1) { ref_count--; return; } */ +retry_atomic_decrement1: + old_ref = g_atomic_int_get (&inode->ref_count); + if (old_ref > 1) + { + if (!g_atomic_int_compare_and_exchange ((int *) &inode->ref_count, old_ref, old_ref - 1)) + goto retry_atomic_decrement1; + } + else + { + if (old_ref <= 0) + { + g_warning ("Can't unref dead inode"); + return; + } + + /* Might be revived from physical_inodes hash by this time, so protect by lock */ + G_LOCK (physical_inodes); + + if (!g_atomic_int_compare_and_exchange ((int *) &inode->ref_count, old_ref, old_ref - 1)) + { + G_UNLOCK (physical_inodes); + goto retry_atomic_decrement1; + } + g_hash_table_remove (physical_inodes, &inode->backing_devino); + + G_UNLOCK (physical_inodes); + + close (inode->fd); + g_free (inode); + } +} + +static XdpDomain * +xdp_domain_ref (XdpDomain *domain) +{ + g_atomic_int_inc (&domain->ref_count); + return domain; +} + +static void +xdp_domain_unref (XdpDomain *domain) +{ + if (g_atomic_int_dec_and_test (&domain->ref_count)) + { + g_free (domain->doc_id); + g_free (domain->app_id); + g_free (domain->doc_path); + g_free (domain->doc_file); + if (domain->inodes) + g_assert (g_hash_table_size (domain->inodes) == 0); + g_clear_pointer (&domain->inodes, g_hash_table_unref); + g_clear_pointer (&domain->parent, xdp_domain_unref); + g_clear_pointer (&domain->parent_inode, xdp_inode_unref); + g_clear_pointer (&domain->tempfiles, g_hash_table_unref); + g_mutex_clear (&domain->tempfile_mutex); + g_free (domain); + } +} + +static gboolean +xdp_domain_is_virtual_type (XdpDomain *domain) +{ + return + domain->type == XDP_DOMAIN_ROOT || + domain->type == XDP_DOMAIN_BY_APP || + domain->type == XDP_DOMAIN_APP; +} + +static XdpDomain * +_xdp_domain_new (XdpDomainType type) +{ + XdpDomain *domain = g_new0 (XdpDomain, 1); + domain->ref_count = 1; + domain->type = type; + g_mutex_init (&domain->tempfile_mutex); + return domain; +} + +static XdpDomain * +xdp_domain_new_root (void) +{ + XdpDomain *domain = _xdp_domain_new (XDP_DOMAIN_ROOT); + domain->inodes = g_hash_table_new (g_str_hash, g_str_equal); + return domain; +} + +static XdpDomain * +xdp_domain_new_by_app (XdpInode *root_inode) +{ + XdpDomain *root_domain = root_inode->domain; + XdpDomain *domain = _xdp_domain_new (XDP_DOMAIN_BY_APP); + domain->parent = xdp_domain_ref (root_domain); + domain->parent_inode = xdp_inode_ref (root_inode); + domain->inodes = g_hash_table_new (g_str_hash, g_str_equal); + return domain; +} + +static XdpDomain * +xdp_domain_new_app (XdpInode *parent_inode, + const char *app_id) +{ + XdpDomain *parent = parent_inode->domain; + XdpDomain *domain = _xdp_domain_new (XDP_DOMAIN_APP); + domain->parent = xdp_domain_ref (parent); + domain->parent_inode = xdp_inode_ref (parent_inode); + domain->app_id = g_strdup (app_id); + domain->inodes = g_hash_table_new (g_str_hash, g_str_equal); + return domain; +} + +static gboolean +xdp_document_domain_is_dir (XdpDomain *domain) +{ + return (domain->doc_flags & DOCUMENT_ENTRY_FLAG_DIRECTORY) != 0; +} + +static XdpDomain * +xdp_domain_new_document (XdpDomain *parent, + const char *doc_id, + PermissionDbEntry *doc_entry) +{ + XdpDomain *domain = _xdp_domain_new (XDP_DOMAIN_DOCUMENT); + const char *db_path; + + domain->parent = xdp_domain_ref (parent); + domain->doc_id = g_strdup (doc_id); + domain->app_id = g_strdup (parent->app_id); + domain->inodes = g_hash_table_new (g_direct_hash, g_direct_equal); + + domain->doc_flags = document_entry_get_flags (doc_entry); + domain->doc_dir_device = document_entry_get_device (doc_entry); + domain->doc_dir_inode = document_entry_get_inode (doc_entry); + + db_path = document_entry_get_path (doc_entry); + if (xdp_document_domain_is_dir (domain)) + { + domain->doc_path = g_strdup (db_path); + domain->doc_file = g_path_get_basename (db_path); + } + else + { + domain->doc_path = g_path_get_dirname (db_path); + domain->doc_file = g_path_get_basename (db_path); + } + + domain->tempfiles = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, (GDestroyNotify)xdp_tempfile_unref); + + return domain; +} + +static gboolean +xdp_document_domain_can_see (XdpDomain *domain) +{ + if (domain->app_id != NULL) + { + g_autoptr(PermissionDbEntry) entry = xdp_lookup_doc (domain->doc_id); + + if (entry == NULL || + !app_can_see_doc (entry, domain->app_id)) + return FALSE; + } + + return TRUE; +} + +static gboolean +xdp_document_domain_can_write (XdpDomain *domain) +{ + if (domain->app_id != NULL) + { + g_autoptr(PermissionDbEntry) entry = xdp_lookup_doc (domain->doc_id); + + if (entry == NULL || + !app_can_write_doc (entry, domain->app_id)) + return FALSE; + } + + return TRUE; +} + +static char ** +xdp_domain_get_inode_keys_as_string (XdpDomain *domain) +{ + char **res; + guint length, i; + + g_assert (domain->type == XDP_DOMAIN_BY_APP); + + G_LOCK (domain_inodes); + + res = (char **)g_hash_table_get_keys_as_array (domain->inodes, &length); + for (i = 0; i < length; i++) + res[i] = g_strdup (res[i]); + + G_UNLOCK (domain_inodes); + + return res; +} + +static XdpTempfile * +xdp_tempfile_ref (XdpTempfile *tempfile) +{ + g_atomic_int_inc (&tempfile->ref_count); + return tempfile; +} + +static XdpTempfile * +xdp_tempfile_new (XdpInode *inode, + const char *name, + const char *tempname) +{ + XdpTempfile *tempfile = g_new0 (XdpTempfile, 1); + + tempfile->ref_count = 1; + tempfile->inode = xdp_inode_ref (inode); + tempfile->name = g_strdup (name); + tempfile->tempname = g_strdup (tempname); + + return tempfile; +} + +static void +xdp_tempfile_unref (XdpTempfile *tempfile) +{ + if (g_atomic_int_dec_and_test (&tempfile->ref_count)) + { + if (tempfile->tempname) + { + g_autofree char *temppath = g_build_filename (tempfile->inode->domain->doc_path, tempfile->tempname, NULL); + (void)unlink (temppath); + } + g_free (tempfile->name); + g_free (tempfile->tempname); + g_clear_pointer (&tempfile->inode, xdp_inode_unref); + g_free (tempfile); + } +} + +static XdpInode * +_xdp_inode_new (void) +{ + XdpInode *inode = g_new0 (XdpInode, 1); + inode->ref_count = 1; + inode->kernel_ref_count = 0; + + return inode; +} + +/* We try to create persistent inode nr based on the backing device and inode nrs, as + * well as the doc/app id (since the same backing dev/ino should be different inodes + * in the fuse filesystem). We do this by hashing the data to generate a value. + * For non-phsyical files or accidental collisions we just pick a free number + * by incrementing. + */ +static guint64 +generate_persistent_ino (DevIno *backing_devino, + const char *doc_id, + const char *app_id) +{ + g_autoptr(GChecksum) checksum = g_checksum_new (G_CHECKSUM_MD5); + guchar digest[64]; + gsize digest_len = 64; + guint64 res; + + g_checksum_update (checksum, (guchar *)backing_devino, sizeof (DevIno)); + if (doc_id) + g_checksum_update (checksum, (guchar *)doc_id, strlen (doc_id)); + if (app_id) + g_checksum_update (checksum, (guchar *)app_id, strlen (app_id)); + + g_checksum_get_digest (checksum, digest, &digest_len); + + res = *(guint64 *)digest; + if (res == FUSE_ROOT_ID || res == 0) + res = FUSE_ROOT_ID + 1; + return res; +} + +/* takes ownership of fd */ +static XdpInode * +xdp_inode_new (XdpDomain *domain, + XdpPhysicalInode *physical) +{ + XdpInode *inode = _xdp_inode_new (); + inode->domain = xdp_domain_ref (domain); + guint64 persistent_ino, try_ino; + + if (physical) + { + inode->physical = xdp_physical_inode_ref (physical); + persistent_ino = generate_persistent_ino (&physical->backing_devino, + domain->doc_id, + domain->app_id); + + } + + G_LOCK (all_inodes); + if (physical) + try_ino = persistent_ino; + else + try_ino = next_virtual_inode++; + + if (g_hash_table_contains (all_inodes, &try_ino)) + try_ino++; + + inode->ino = try_ino; + g_hash_table_insert (all_inodes, &inode->ino, inode); + G_UNLOCK (all_inodes); + + return inode; +} + +static ino_t +xdp_inode_to_ino (XdpInode *inode) +{ + return inode->ino; +} + +/* This is called on kernel upcalls, so it *should* be guaranteed that the inode exists due to the kernel refs */ +static XdpInode * +xdp_inode_from_ino (ino_t ino) +{ + XdpInode *inode; + + G_LOCK (all_inodes); + inode = g_hash_table_lookup (all_inodes, &ino); + G_UNLOCK (all_inodes); + + g_assert (inode != NULL); + + /* Its safe to ref it here because we know it exists outside the lock due to the kernel refs */ + return xdp_inode_ref (inode); +} + +static XdpInode * +xdp_inode_ref (XdpInode *inode) +{ + g_atomic_int_inc (&inode->ref_count); + return inode; +} + +static void +xdp_inode_unref (XdpInode *inode) +{ + gint old_ref; + XdpDomain *domain; + + /* here we want to atomically do: if (ref_count>1) { ref_count--; return; } */ +retry_atomic_decrement1: + old_ref = g_atomic_int_get (&inode->ref_count); + if (old_ref > 1) + { + if (!g_atomic_int_compare_and_exchange ((int *) &inode->ref_count, old_ref, old_ref - 1)) + goto retry_atomic_decrement1; + } + else + { + if (old_ref <= 0) + { + g_warning ("Can't unref dead inode"); + return; + } + + /* Might be revived from domain->inodes hash by this time, so protect by lock */ + G_LOCK (domain_inodes); + + if (!g_atomic_int_compare_and_exchange ((int *) &inode->ref_count, old_ref, old_ref - 1)) + { + G_UNLOCK (domain_inodes); + goto retry_atomic_decrement1; + } + + domain = inode->domain; + + if (domain->type == XDP_DOMAIN_APP) + g_hash_table_remove (domain->parent->inodes, domain->app_id); + else if (domain->type == XDP_DOMAIN_DOCUMENT) + { + if (inode->physical) + { + g_hash_table_remove (domain->inodes, inode->physical); + } + else + g_hash_table_remove (domain->parent->inodes, domain->doc_id); + } + + /* Run this under domain_inodes lock to avoid race condition in ensure_docdir_inode + xdp_inode_new + * where we don't want a domain->inode lookup to fail, but then an all_inode lookup to succeed + * when looking for an ino collision + * + * Note: After the domain->inodes removal and here we don't allow resurrection, but we may + * still race with an all_inodes lookup (e.g. in xdp_fuse_lookup_id_for_inode), which *is* + * allowed and it can read the inode fields (while the lock is held) as they are still valid. + **/ + G_LOCK (all_inodes); + g_hash_table_remove (all_inodes, &inode->ino); + G_UNLOCK (all_inodes); + + G_UNLOCK (domain_inodes); + + /* By now we have no refs outstanding and no way to get at the inode, so free it */ + + g_clear_pointer (&inode->domain_root_inode, xdp_inode_unref); + g_clear_pointer (&inode->physical, xdp_physical_inode_unref); + xdp_domain_unref (inode->domain); + g_free (inode); + } + +} + +static XdpInode * +xdp_inode_kernel_ref (XdpInode *inode) +{ + int old; + + old = g_atomic_int_add (&inode->kernel_ref_count, 1); + + if (old == 0) + xdp_inode_ref (inode); + return inode; +} + +static void +xdp_inode_kernel_unref (XdpInode *inode, unsigned long count) +{ + gint old_ref, new_ref; + + retry_atomic_decrement1: + old_ref = g_atomic_int_get (&inode->kernel_ref_count); + if (old_ref < count) + { + g_warning ("Can't kernel_unref inode with no kernel refs"); + return; + } + new_ref = old_ref - count; + if (!g_atomic_int_compare_and_exchange (&inode->kernel_ref_count, old_ref, new_ref)) + goto retry_atomic_decrement1; + + if (new_ref == 0) + xdp_inode_unref (inode); +} + +static int +verify_doc_dir_devino (int dirfd, XdpDomain *doc_domain) +{ + struct stat buf; + + if (fstat (dirfd, &buf) != 0) + return -errno; + + if (buf.st_ino != doc_domain->doc_dir_inode || + buf.st_dev != doc_domain->doc_dir_device) + return -ENOENT; + + return 0; + } + +/* Only for toplevel dirs, not this is a bit weird for toplevel dir + inodes as it returns the dir itself which isn't really the dirfd + for that (nonphysical) inode */ +static int +xdp_nonphysical_document_inode_opendir (XdpInode *inode) +{ + XdpDomain *domain = inode->domain; + xdp_autofd int dirfd = -1; + int res; + + g_assert (domain->type == XDP_DOMAIN_DOCUMENT); + g_assert (inode->physical == NULL); + + dirfd = open (domain->doc_path, O_PATH | O_DIRECTORY); + if (dirfd < 0) + return -errno; + + res = verify_doc_dir_devino (dirfd, domain); + if (res != 0) + return res; + + return xdp_steal_fd (&dirfd); +} + +static int +xdp_document_inode_ensure_dirfd (XdpInode *inode, + int *close_fd_out) +{ + int close_fd; + + g_assert (inode->domain->type == XDP_DOMAIN_DOCUMENT); + + *close_fd_out = -1; + + if (inode->physical) + return inode->physical->fd; + else + { + if (xdp_document_domain_is_dir (inode->domain)) + { + /* There is no dirfd for the toplevel dirs, happens for example + if renaming into toplevel, so just return EPERM */ + return -EPERM; + } + + close_fd = xdp_nonphysical_document_inode_opendir (inode); + if (close_fd < 0) + return close_fd; + + *close_fd_out = close_fd; + return close_fd; + } +} + +static gboolean +open_flags_has_write (int open_flags) +{ + return + (open_flags & O_ACCMODE) == O_WRONLY || + (open_flags & O_ACCMODE) == O_RDWR || + open_flags & O_TRUNC; +} + +static void +gen_temp_name (gchar *tmpl) +{ + g_return_if_fail (tmpl != NULL); + const size_t len = strlen (tmpl); + g_return_if_fail (len >= 6); + + static const char letters[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + static const int NLETTERS = sizeof (letters) - 1; + + char *XXXXXX = tmpl + (len - 6); + for (int i = 0; i < 6; i++) + XXXXXX[i] = letters[g_random_int_range(0, NLETTERS)]; +} + +static int +open_temp_at (int dirfd, + const char *orig_name, + char **name_out, + mode_t mode) +{ + int fd; + int errsv; + const guint count_max = 100; + g_autofree char *tmp = g_strconcat (".xdp-", orig_name, "-XXXXXX", NULL); + + for (int count = 0; count < count_max; count++) + { + gen_temp_name (tmp); + + fd = openat (dirfd, tmp, O_CREAT|O_EXCL|O_NOFOLLOW|O_NOCTTY|O_RDWR, mode); + errsv = errno; + if (fd < 0) + { + if (errsv == EEXIST) + continue; + else + return -errsv; + } + + *name_out = g_steal_pointer (&tmp); + return fd; + } + + return -EEXIST; +} + +/* allocates tempfile for existing file, + Called with tempfile lock held, sets errno */ +static int +get_tempfile_for (XdpInode *parent, + XdpDomain *domain, + const char *name, + int dirfd, + const char *tmpname, + XdpTempfile **tempfile_out) +{ + g_autoptr(XdpTempfile) tempfile = NULL; + g_autoptr(XdpInode) inode = NULL; + xdp_autofd int o_path_fd = -1; + int res; + + if (tempfile_out != NULL) + *tempfile_out = NULL; + + o_path_fd = openat (dirfd, tmpname, O_PATH, 0); + if (o_path_fd == -1) + return -errno; + + res = ensure_docdir_inode (parent, xdp_steal_fd (&o_path_fd), NULL, &inode); /* passed ownership of o_path_fd */ + if (res != 0) + return res; + + tempfile = xdp_tempfile_new (inode, name, tmpname); + + /* This is atomic, because we're called with the lock held */ + g_hash_table_replace (domain->tempfiles, tempfile->name, xdp_tempfile_ref (tempfile)); + + if (tempfile_out) + *tempfile_out = g_steal_pointer (&tempfile); + return 0; +} + +/* Creates a new file on disk, + Called with tempfile lock held, sets errno */ +static int +create_tempfile (XdpInode *parent, + XdpDomain *domain, + const char *name, + int dirfd, + mode_t mode, + XdpTempfile **tempfile_out) +{ + g_autoptr(XdpInode) inode = NULL; + g_autofree char *real_fd_path = NULL; + xdp_autofd int real_fd = -1; + xdp_autofd int o_path_fd = -1; + g_autoptr(XdpTempfile) tempfile = NULL; + g_autofree char *tmpname = NULL; + int res; + + if (tempfile_out != NULL) + *tempfile_out = NULL; + + real_fd = open_temp_at (dirfd, name, &tmpname, mode); + if (real_fd < 0) + return real_fd; + + real_fd_path = fd_to_path (real_fd); + o_path_fd = open (real_fd_path, O_PATH, 0); + if (o_path_fd == -1) + return -errno; + + /* We can close the tmpfd early */ + close (xdp_steal_fd (&real_fd)); + + res = ensure_docdir_inode (parent, xdp_steal_fd (&o_path_fd), NULL, &inode); /* passed ownership of o_path_fd */ + if (res != 0) + return res; + + tempfile = xdp_tempfile_new (inode, name, tmpname); + + /* This is atomic, because we're called with the lock held */ + g_hash_table_replace (domain->tempfiles, tempfile->name, xdp_tempfile_ref (tempfile)); + + if (tempfile_out) + *tempfile_out = g_steal_pointer (&tempfile); + return 0; +} + +static int +xdp_document_inode_open_child_fd (XdpInode *inode, const char *name, int open_flags, mode_t mode) +{ + XdpDomain *domain = inode->domain; + XdpTempfile *tempfile_lookup = NULL; + g_autoptr(XdpTempfile) tempfile = NULL; + int tempfile_res = 0; + xdp_autofd int fd = -1; + + g_assert (domain->type == XDP_DOMAIN_DOCUMENT); + + if (!xdp_document_domain_can_write (domain) && + (open_flags_has_write (open_flags) || + (open_flags & O_CREAT) != 0)) + return -EACCES; + + if (inode->physical) + { + fd = openat (inode->physical->fd, name, open_flags, mode); + if (fd == -1) + return -errno; + + return xdp_steal_fd (&fd); + } + else + { + xdp_autofd int dirfd = -1; + + if (xdp_document_domain_is_dir (domain)) + { + if (strcmp (name, domain->doc_file) == 0) + { + /* Ensure toplevel dir exist and is right */ + dirfd = xdp_nonphysical_document_inode_opendir (inode); + if (dirfd < 0) + return dirfd; + + fd = openat (dirfd, ".", open_flags, mode); + if (fd == -1) + return -errno; + return xdp_steal_fd (&fd); + } + } + else + { + /* Ensure parent dir exist and is right */ + dirfd = xdp_nonphysical_document_inode_opendir (inode); + if (dirfd < 0) + return dirfd; + + if (strcmp (name, domain->doc_file) == 0) + { + fd = openat (dirfd, name, open_flags, mode); + if (fd == -1) + return -errno; + return xdp_steal_fd (&fd); + } + + /* Not main file, maybe a temporary file? */ + + g_mutex_lock (&domain->tempfile_mutex); + + tempfile_lookup = g_hash_table_lookup (domain->tempfiles, name); + if (tempfile_lookup) + { + if ((open_flags & O_CREAT) && (open_flags & O_EXCL)) + tempfile_res = -EEXIST; + else + tempfile = xdp_tempfile_ref (tempfile_lookup); + } + else if (open_flags & O_CREAT) + { + tempfile_res = create_tempfile (inode, domain, name, dirfd, mode, &tempfile); + } + + g_mutex_unlock (&domain->tempfile_mutex); + + if (tempfile) + { + g_autofree char *fd_path = fd_to_path (tempfile->inode->physical->fd); + fd = open (fd_path, open_flags & ~(O_CREAT|O_EXCL|O_NOFOLLOW), mode); + if (fd == -1) + return -errno; + + return xdp_steal_fd (&fd); + } + else + { + if (tempfile_res != 0) + return tempfile_res; + } + } + } + + return -ENOENT; +} + +/* Returns /proc/self/fds/$fd path for O_PATH fd or toplevel path */ +static char * +xdp_document_inode_get_self_as_path (XdpInode *inode) +{ + g_assert (inode->domain->type == XDP_DOMAIN_DOCUMENT); + + if (inode->physical) + return fd_to_path (inode->physical->fd); + else + { + if (xdp_document_domain_is_dir (inode->domain)) + return NULL; + return g_strdup (inode->domain->doc_path); + } +} + +static void +tweak_statbuf_for_document_inode (XdpInode *inode, + struct stat *buf) +{ + XdpDomain *domain = inode->domain; + + g_assert (domain->type == XDP_DOMAIN_DOCUMENT); + + buf->st_ino = xdp_inode_to_ino (inode); + + /* Remove setuid/setgid/sticky flags */ + buf->st_mode &= ~(S_ISUID|S_ISGID|S_ISVTX); + + if (!xdp_document_domain_can_write (domain)) + buf->st_mode &= ~(0222); +} + +static void +xdp_reply_err (const char *op, fuse_req_t req, int err) +{ + if (err != 0) + { + const char *errname = NULL; + switch (err) + { + case ESTALE: + errname = "ESTALE"; + break; + case EEXIST: + errname = "EEXIST"; + break; + case ENOENT: + errname = "ENOENT"; + break; + case EPERM: + errname = "EPERM"; + break; + case EACCES: + errname = "EACCES"; + break; + case EINVAL: + errname = "EINVAL"; + break; + default: + errname = NULL; + } + if (errname != NULL) + g_debug ("%s -> error %s", op, errname); + else + g_debug ("%s -> error %d", op, err); + } + fuse_reply_err (req, err); +} + +typedef enum { + CHECK_CAN_WRITE = 1 << 0, + CHECK_IS_DIRECTORY = 1 << 1, + CHECK_IS_PHYSICAL = 1 << 2, + CHECK_IS_PHYSICAL_IF_DIR = 1 << 3, +} XdpDocumentChecks; + +static gboolean +xdp_document_inode_checks (const char *op, + fuse_req_t req, + XdpInode *inode, + XdpDocumentChecks checks) +{ + XdpDomain *domain = inode->domain; + gboolean check_is_directory = (checks & CHECK_IS_DIRECTORY) != 0; + gboolean check_can_write = (checks & CHECK_CAN_WRITE) != 0; + gboolean check_is_physical = (checks & CHECK_IS_PHYSICAL) != 0; + gboolean check_is_physical_if_dir = (checks & CHECK_IS_PHYSICAL_IF_DIR) != 0; + + if (domain->type != XDP_DOMAIN_DOCUMENT) + { + xdp_reply_err (op, req, EPERM); + return FALSE; + } + + /* We allowed the inode lookup to succeed, but maybe the permissions changed since then */ + if (!xdp_document_domain_can_see (domain)) + { + xdp_reply_err (op, req, EACCES); + return FALSE; + } + + if (check_is_directory && !xdp_document_domain_is_dir (domain)) + { + xdp_reply_err (op, req, EPERM); + return FALSE; + } + + if (check_can_write && !xdp_document_domain_can_write (domain)) + { + xdp_reply_err (op, req, EACCES); + return FALSE; + } + + if (check_is_physical_if_dir && xdp_document_domain_is_dir (domain)) + check_is_physical = TRUE; + + if (check_is_physical && inode->physical == NULL) + { + xdp_reply_err (op, req, EPERM); + return FALSE; + } + + return TRUE; +} + +static void +stat_virtual_inode (XdpInode *inode, + struct stat *buf) +{ + memset (buf, 0, sizeof (struct stat)); + buf->st_ino = xdp_inode_to_ino (inode); + buf->st_uid = my_uid; + buf->st_gid = my_gid; + + switch (inode->domain->type) + { + case XDP_DOMAIN_ROOT: + case XDP_DOMAIN_BY_APP: + case XDP_DOMAIN_APP: + buf->st_mode = S_IFDIR | NON_DOC_DIR_PERMS; + buf->st_nlink = 2; + break; + case XDP_DOMAIN_DOCUMENT: + if (xdp_document_domain_is_dir (inode->domain)) + buf->st_mode = S_IFDIR | DOC_DIR_PERMS_DIR; + else + buf->st_mode = S_IFDIR | DOC_DIR_PERMS_FILE; + buf->st_nlink = 2; + + /* Remove perms if not writable */ + if (inode->domain->app_id != NULL) + { + g_autoptr(PermissionDbEntry) entry = xdp_lookup_doc (inode->domain->doc_id); + if (entry == NULL || !app_can_write_doc (entry, inode->domain->app_id)) + buf->st_mode &= ~(0222); + } + break; + + default: + g_assert_not_reached (); + break; + } +} + +static void +xdp_fuse_getattr (fuse_req_t req, + fuse_ino_t ino, + struct fuse_file_info *fi) +{ + g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino); + XdpDomain *domain = inode->domain; + struct stat buf; + int res; + double attr_valid_time = 0.0;/* Time in secs for attribute validation */ + const char *op = "GETATTR"; + + g_debug ("GETATTR %lx", ino); + + if (xdp_domain_is_virtual_type (domain)) + { + stat_virtual_inode (inode, &buf); + fuse_reply_attr (req, &buf, attr_valid_time); + return; + } + + g_assert (domain->type == XDP_DOMAIN_DOCUMENT); + + if (inode->physical) + res = fstatat (inode->physical->fd, "", &buf, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW); + else + { + stat_virtual_inode (inode, &buf); + res = 0; + } + if (res == -1) + return xdp_reply_err (op, req, errno); + + tweak_statbuf_for_document_inode (inode, &buf); + + fuse_reply_attr (req, &buf, attr_valid_time); +} + +static void +xdp_fuse_setattr (fuse_req_t req, + fuse_ino_t ino, + struct stat *attr, + int to_set, + struct fuse_file_info *fi) +{ + g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino); + g_autofree char *to_set_string = setattr_flags_to_string (to_set); + struct stat buf; + double attr_valid_time = 0.0;/* Time in secs for attribute validation */ + int res; + const char *op = "SETATTR"; + + g_debug ("SETATTR %lx %s", ino, to_set_string); + + if (!xdp_document_inode_checks (op, req, inode, + CHECK_CAN_WRITE | CHECK_IS_PHYSICAL)) + return; + + /* Truncate */ + if (to_set & FUSE_SET_ATTR_SIZE) + { + g_autofree char *path = NULL; + XdpFile *file = (XdpFile *)fi->fh; + + if (file) + { + res = ftruncate (file->fd, attr->st_size); + if (res == -1) + res = -errno; + } + else if (inode->physical) + { + path = fd_to_path (inode->physical->fd); + res = truncate (path, attr->st_size); + if (res == -1) + res = -errno; + } + else + { + res = -EISDIR; + } + + if (res != 0) + return xdp_reply_err (op, req, -res); + } + + if (to_set & (FUSE_SET_ATTR_ATIME | FUSE_SET_ATTR_MTIME)) + { + struct timespec times[2] = { {0, UTIME_OMIT}, {0, UTIME_OMIT} }; /* 0 = atime, 1 = mtime */ + g_autofree char *path = NULL; + + if (to_set & FUSE_SET_ATTR_ATIME_NOW) + times[0].tv_nsec = UTIME_NOW; + else if (to_set & FUSE_SET_ATTR_ATIME) + times[0] = attr->st_atim; + + if (to_set & FUSE_SET_ATTR_MTIME_NOW) + times[1].tv_nsec = UTIME_NOW; + else if (to_set & FUSE_SET_ATTR_MTIME) + times[1] = attr->st_mtim; + + if (inode->physical) + { + path = fd_to_path (inode->physical->fd); + res = utimensat (AT_FDCWD, path, times, 0); + } + else + res = utimensat (AT_FDCWD, inode->domain->doc_path, times, 0); /* follow symlink here */ + + if (res != 0) + return xdp_reply_err (op, req, errno); + } + + if (to_set & (FUSE_SET_ATTR_UID | FUSE_SET_ATTR_GID)) + { + g_autofree char *path = NULL; + uid_t uid = -1; + gid_t gid = -1; + + if (to_set & FUSE_SET_ATTR_UID) + uid = attr->st_uid; + + if (to_set & FUSE_SET_ATTR_GID) + gid = attr->st_gid; + + if (inode->physical) + { + path = fd_to_path (inode->physical->fd); + res = chown (path, uid, gid); + if (res == -1) + res = -errno; + } + else + { + res = -EACCES; + } + + if (res != 0) + return xdp_reply_err (op, req, -res); + } + + if (to_set & (FUSE_SET_ATTR_MODE)) + { + g_autofree char *path = NULL; + + if (inode->physical) + { + path = fd_to_path (inode->physical->fd); + res = chmod (path, attr->st_mode); + if (res == -1) + res = -errno; + } + else + { + res = -EACCES; + } + + if (res != 0) + return xdp_reply_err (op, req, -res); + } + + if (inode->physical) + res = fstatat (inode->physical->fd, "", &buf, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW); + else + res = stat (inode->domain->doc_path, &buf); /* Follow symlinks here */ + + if (res != 0) + return xdp_reply_err (op, req, errno); + + tweak_statbuf_for_document_inode (inode, &buf); + + fuse_reply_attr (req, &buf, attr_valid_time); +} + +static void +prepare_reply_entry (XdpInode *inode, + struct stat *buf, + struct fuse_entry_param *e) +{ + xdp_inode_kernel_ref (inode); /* Ref given to the kernel, returned in xdp_forget() */ + e->ino = xdp_inode_to_ino (inode); + e->generation = 1; + e->attr = *buf; + e->attr_timeout = 0.0; /* attribute timeout */ + e->entry_timeout = 0.0; /* dentry timeout */ +} + +static void +prepare_reply_virtual_entry (XdpInode *inode, + struct fuse_entry_param *e) +{ + stat_virtual_inode (inode, &e->attr); + + xdp_inode_kernel_ref (inode); /* Ref given to the kernel, returned in xdp_forget() */ + e->ino = xdp_inode_to_ino (inode); + e->generation = 1; + + /* Cache virtual dirs */ + e->attr_timeout = 60.0; /* attribute timeout */ + e->entry_timeout = 60.0; /* dentry timeout */ +} + +static void +abort_reply_entry (struct fuse_entry_param *e) +{ + XdpInode *inode = xdp_inode_from_ino (e->ino); + xdp_inode_kernel_unref (inode, 1); +} + +static int +ensure_docdir_inode (XdpInode *parent, + int o_path_fd_in, /* Takes ownership */ + struct fuse_entry_param *e, + XdpInode **inode_out) +{ + XdpDomain *domain = parent->domain; + g_autoptr(XdpPhysicalInode) physical = NULL; + g_autoptr(XdpInode) inode = NULL; + xdp_autofd int o_path_fd = o_path_fd_in; + struct stat buf; + int res; + + res = fstatat (o_path_fd, "", &buf, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW); + if (res == -1) + return -errno; + + /* non-directory documents only support regular files */ + if (!xdp_document_domain_is_dir (domain) && !S_ISREG(buf.st_mode)) + return -ENOENT; + + physical = ensure_physical_inode (buf.st_dev, buf.st_ino, xdp_steal_fd (&o_path_fd)); /* passed ownership of fd */ + + G_LOCK(domain_inodes); + inode = g_hash_table_lookup (domain->inodes, physical); + if (inode != NULL) + inode = xdp_inode_ref (inode); + else + { + inode = xdp_inode_new (domain, physical); + if (parent->domain_root_inode) + inode->domain_root_inode = xdp_inode_ref (parent->domain_root_inode); + else + inode->domain_root_inode = xdp_inode_ref (parent); + g_hash_table_insert (domain->inodes, physical, inode); + } + G_UNLOCK(domain_inodes); + + if (e) + { + tweak_statbuf_for_document_inode (inode, &buf); + prepare_reply_entry (inode, &buf, e); + } + + if (inode_out) + *inode_out = g_steal_pointer (&inode); + + return 0; +} + +static int +ensure_docdir_inode_by_name (XdpInode *parent, + int dirfd, + const char *name, + struct fuse_entry_param *e) +{ + int o_path_fd; + + o_path_fd = openat (dirfd, name, O_PATH|O_NOFOLLOW, 0); + if (o_path_fd == -1) + return -errno; + + return ensure_docdir_inode (parent, o_path_fd, e, NULL); /* Takes ownershif of o_path_fd */ +} + + +static XdpInode * +ensure_by_app_inode (XdpInode *by_app_inode, + const char *app_id) +{ + XdpDomain *by_app_domain = by_app_inode->domain; + g_autoptr(XdpInode) inode = NULL; + + if (!xdp_is_valid_app_id (app_id)) + return NULL; + + G_LOCK(domain_inodes); + inode = g_hash_table_lookup (by_app_domain->inodes, app_id); + if (inode != NULL) + inode = xdp_inode_ref (inode); + else + { + g_autoptr(XdpDomain) app_domain = xdp_domain_new_app (by_app_inode, app_id); + inode = xdp_inode_new (app_domain, NULL); + g_hash_table_insert (by_app_domain->inodes, app_domain->app_id, inode); + } + G_UNLOCK(domain_inodes); + + return g_steal_pointer (&inode); +} + +static XdpInode * +ensure_doc_inode (XdpInode *parent, + const char *doc_id) +{ + g_autoptr(XdpInode) inode = NULL; + g_autoptr(PermissionDbEntry) doc_entry = NULL; + XdpDomain *parent_domain = parent->domain; + + doc_entry = xdp_lookup_doc (doc_id); + + if (doc_entry == NULL || + (parent_domain->app_id && + !app_can_see_doc (doc_entry, parent_domain->app_id))) + return NULL; + + G_LOCK(domain_inodes); + inode = g_hash_table_lookup (parent_domain->inodes, doc_id); + if (inode != NULL) + inode = xdp_inode_ref (inode); + else + { + g_autoptr(XdpDomain) doc_domain = xdp_domain_new_document (parent_domain, doc_id, doc_entry); + doc_domain->parent_inode = xdp_inode_ref (parent); + inode = xdp_inode_new (doc_domain, NULL); + g_hash_table_insert (parent_domain->inodes, doc_domain->doc_id, inode); + } + G_UNLOCK(domain_inodes); + + return g_steal_pointer (&inode); +} + +static gboolean +invalidate_doc_domain (gpointer user_data) +{ + g_autoptr(XdpDomain) doc_domain = user_data; + ino_t parent_ino; + + g_atomic_int_set (&doc_domain->doc_queued_invalidate, 0); + + parent_ino = xdp_inode_to_ino (doc_domain->parent_inode); + + XDP_AUTOLOCK (session); + if (session && g_atomic_int_get (&doc_domain->parent_inode->kernel_ref_count) > 0) + fuse_lowlevel_notify_inval_entry (session, parent_ino, doc_domain->doc_id, + strlen (doc_domain->doc_id)); + + return FALSE; +} + +/* Queue an inval_entry call on this domain, thereby freeing all unused inodes + * in the dcache which will free up a bunch of O_PATH fds in the fuse implementation + */ +static void +doc_domain_queue_entry_invalidate (XdpDomain *doc_domain) +{ + int old = g_atomic_int_get (&doc_domain->doc_queued_invalidate); + if (old != 0) + return; + + if (!g_atomic_int_compare_and_exchange (&doc_domain->doc_queued_invalidate, old, 1)) + return; // Someone else set it to 1, return + + g_timeout_add (1000, invalidate_doc_domain, xdp_domain_ref (doc_domain)); +} + +static void +xdp_fuse_lookup (fuse_req_t req, + fuse_ino_t parent_ino, + const char *name) +{ + g_autoptr(XdpInode) parent = xdp_inode_from_ino (parent_ino); + XdpDomain *parent_domain = parent->domain; + g_autoptr(XdpInode) inode = NULL; + struct fuse_entry_param e; + int res, fd; + int open_flags = O_PATH|O_NOFOLLOW; + const char *op = "LOOKUP"; + + g_debug ("LOOKUP %lx:%s", parent_ino, name); + + if (strcmp (name, ".") == 0 || strcmp (name, "..") == 0) + { + /* We don't set FUSE_CAP_EXPORT_SUPPORT, so should not get + * here. But lets make sure we never ever resolve them as that + * could be a security issue by escaping the root. */ + return xdp_reply_err (op, req, ESTALE); + } + + if (xdp_domain_is_virtual_type (parent_domain)) + { + switch (parent_domain->type) + { + case XDP_DOMAIN_ROOT: + if (strcmp (name, BY_APP_NAME) == 0) + inode = xdp_inode_ref (by_app_inode); + else + inode = ensure_doc_inode (parent, name); + break; + case XDP_DOMAIN_BY_APP: + inode = ensure_by_app_inode (parent, name); + break; + case XDP_DOMAIN_APP: + inode = ensure_doc_inode (parent, name); + break; + default: + g_assert_not_reached (); + } + + if (inode == NULL) + return xdp_reply_err (op, req, ENOENT); + + prepare_reply_virtual_entry (inode, &e); + } + else + { + g_assert (parent_domain->type == XDP_DOMAIN_DOCUMENT); + + fd = xdp_document_inode_open_child_fd (parent, name, open_flags, 0); + if (fd < 0) + return xdp_reply_err (op, req, -fd); + + res = ensure_docdir_inode (parent, fd, &e, NULL); /* Takes ownershif of fd */ + if (res != 0) + return xdp_reply_err (op, req, -res); + + doc_domain_queue_entry_invalidate (parent_domain); + } + + g_debug ("LOOKUP %lx:%s => %lx", parent_ino, name, e.ino); + + if (fuse_reply_entry (req, &e) == -ENOENT) + abort_reply_entry (&e); +} + +static XdpFile * +xdp_file_new (int fd) +{ + XdpFile *file = g_new0 (XdpFile, 1); + file->fd = fd; + return file; +} + +static void +xdp_file_free (XdpFile *file) +{ + close (file->fd); + g_free (file); +} + +static void +xdp_fuse_open (fuse_req_t req, + fuse_ino_t ino, + struct fuse_file_info *fi) +{ + g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino); + int open_flags = fi->flags; + g_autofree char *open_flags_string = open_flags_to_string (open_flags); + int fd; + g_autofree char *path = NULL; + XdpFile *file = NULL; + XdpDocumentChecks checks; + const char *op = "OPEN"; + + g_debug ("OPEN %lx %s", ino, open_flags_string); + + checks = CHECK_IS_PHYSICAL; + if (open_flags_has_write (open_flags)) + checks |= CHECK_CAN_WRITE; + + /* Note: open_flags guaranteed to exclude O_CREAT, O_EXCL */ + if (!xdp_document_inode_checks (op, req, inode, checks)) + return; + + path = fd_to_path (inode->physical->fd); + fd = open (path, open_flags, 0); + if (fd == -1) + return xdp_reply_err (op, req, errno); + + file = xdp_file_new (fd); + + fi->fh = (gsize)file; + if (fuse_reply_open (req, fi) == -ENOENT) + { + /* The open syscall was interrupted, so it must be cancelled */ + xdp_file_free (file); + } +} + +static void +xdp_fuse_create (fuse_req_t req, + fuse_ino_t parent_ino, + const char *filename, + mode_t mode, + struct fuse_file_info *fi) +{ + g_autoptr(XdpInode) parent = xdp_inode_from_ino (parent_ino); + int open_flags = fi->flags; + g_autofree char *open_flags_string = open_flags_to_string (open_flags); + struct fuse_entry_param e; + int res; + xdp_autofd int fd = -1; + xdp_autofd int o_path_fd = -1; + g_autofree char *fd_path = NULL; + XdpFile *file = NULL; + const char *op = "CREATE"; + + g_debug ("CREATE %lx %s %s, 0%o", parent_ino, filename, open_flags_string, mode); + + if (!xdp_document_inode_checks (op, req, parent, + CHECK_CAN_WRITE | + CHECK_IS_PHYSICAL_IF_DIR)) + return; + + fd = xdp_document_inode_open_child_fd (parent, filename, open_flags, mode); + if (fd < 0) + return xdp_reply_err (op, req, -fd); + + fd_path = fd_to_path (fd); + o_path_fd = open (fd_path, O_PATH, 0); + if (o_path_fd < 0) + return xdp_reply_err (op, req, errno); + + res = ensure_docdir_inode (parent, xdp_steal_fd (&o_path_fd), &e, NULL); /* Takes ownershif of o_path_fd */ + if (res != 0) + return xdp_reply_err (op, req, -res); + + file = xdp_file_new (xdp_steal_fd (&fd)); /* Takes ownership of fd */ + + fi->fh = (gsize)file; + if (fuse_reply_create (req, &e, fi) == -ENOENT) + { + /* The open syscall was interrupted, so it must be cancelled */ + xdp_file_free (file); + abort_reply_entry (&e); + } +} + +static void +xdp_fuse_read (fuse_req_t req, + fuse_ino_t ino, + size_t size, + off_t off, + struct fuse_file_info *fi) +{ + struct fuse_bufvec buf = FUSE_BUFVEC_INIT(size); + XdpFile *file = (XdpFile *)fi->fh; + + g_debug ("READ %lx size %ld off %ld", ino, size, off); + + buf.buf[0].flags = FUSE_BUF_IS_FD | FUSE_BUF_FD_SEEK; + buf.buf[0].fd = file->fd; + buf.buf[0].pos = off; + + fuse_reply_data (req, &buf, FUSE_BUF_SPLICE_MOVE); +} + + +static void +xdp_fuse_write (fuse_req_t req, + fuse_ino_t ino, + const char *buf, + size_t size, + off_t off, + struct fuse_file_info *fi) +{ + XdpFile *file = (XdpFile *)fi->fh; + ssize_t res; + const char *op = "WRITE"; + + g_debug ("WRITE %lx size %ld off %ld", ino, size, off); + + res = pwrite (file->fd, buf, size, off); + + if (res >= 0) + fuse_reply_write (req, res); + else + xdp_reply_err (op, req, errno); +} + +static void +xdp_fuse_write_buf (fuse_req_t req, + fuse_ino_t ino, + struct fuse_bufvec *bufv, + off_t off, + struct fuse_file_info *fi) +{ + XdpFile *file = (XdpFile *)fi->fh; + struct fuse_bufvec dst = FUSE_BUFVEC_INIT(fuse_buf_size(bufv)); + ssize_t res; + const char *op = "WRITEBUF"; + + g_debug ("WRITEBUF %lx off %ld", ino, off); + + dst.buf[0].flags = FUSE_BUF_IS_FD | FUSE_BUF_FD_SEEK; + dst.buf[0].fd = file->fd; + dst.buf[0].pos = off; + + res = fuse_buf_copy (&dst, bufv, FUSE_BUF_SPLICE_NONBLOCK); + if (res >= 0) + fuse_reply_write (req, res); + else + xdp_reply_err (op, req, errno); +} + +static void +xdp_fuse_fsync (fuse_req_t req, + fuse_ino_t ino, + int datasync, + struct fuse_file_info *fi) +{ + XdpFile *file = (XdpFile *)fi->fh; + int res; + const char *op = "FSYNC"; + + g_debug ("FSYNC %lx", ino); + + if (datasync) + res = fdatasync (file->fd); + else + res = fsync (file->fd); + if (res == 0) + xdp_reply_err (op, req, 0); + else + xdp_reply_err (op, req, errno); +} + +static void +xdp_fuse_fallocate (fuse_req_t req, + fuse_ino_t ino, + int mode, + off_t offset, + off_t length, + struct fuse_file_info *fi) +{ + XdpFile *file = (XdpFile *)fi->fh; + int res; + const char *op = "FALLOCATE"; + + g_debug ("FALLOCATE %lx", ino); + +#ifdef __linux__ + res = fallocate (file->fd, mode, offset, length); +#else + res = posix_fallocate (file->fd, offset, length); +#endif + + if (res == 0) + xdp_reply_err (op, req, 0); + else + xdp_reply_err (op, req, errno); +} + +static void +xdp_fuse_flush (fuse_req_t req, + fuse_ino_t ino, + struct fuse_file_info *fi) +{ + const char *op = "FLUSH"; + + g_debug ("FLUSH %lx", ino); + xdp_reply_err (op, req, 0); +} + +static void +xdp_fuse_release (fuse_req_t req, + fuse_ino_t ino, + struct fuse_file_info *fi) +{ + XdpFile *file = (XdpFile *)fi->fh; + const char *op = "RELEASE"; + + g_debug ("RELEASE %lx", ino); + + xdp_file_free (file); + + xdp_reply_err (op, req, 0); +} + +static void +forget_one (fuse_ino_t ino, + unsigned long nlookup) +{ + g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino); + + g_debug ("FORGET %lx %ld", ino, nlookup); + xdp_inode_kernel_unref (inode, nlookup); +} + +static void +xdp_fuse_forget (fuse_req_t req, + fuse_ino_t ino, + unsigned long nlookup) +{ + forget_one (ino, nlookup); + fuse_reply_none (req); +} + +static void +xdp_fuse_forget_multi (fuse_req_t req, + size_t count, + struct fuse_forget_data *forgets) +{ + size_t i; + + g_debug ("FORGET_MULTI %ld", count); + + for (i = 0; i < count; i++) + forget_one (forgets[i].ino, forgets[i].nlookup); + + fuse_reply_none (req); +} + +static void +xdp_dir_free (XdpDir *d) +{ + if (d->dir) + closedir (d->dir); + g_free (d->dirbuf); + g_free (d); +} + +static void +xdp_dir_add (XdpDir *d, + fuse_req_t req, + const char *name, + mode_t mode) +{ + struct stat stbuf; + + size_t oldsize = d->dirbuf_size; + + d->dirbuf_size += fuse_add_direntry (req, NULL, 0, name, NULL, 0); + d->dirbuf = (char *) g_realloc (d->dirbuf, d->dirbuf_size); + memset (&stbuf, 0, sizeof (stbuf)); + stbuf.st_ino = FUSE_UNKNOWN_INO; + stbuf.st_mode = mode; + fuse_add_direntry (req, d->dirbuf + oldsize, + d->dirbuf_size - oldsize, + name, &stbuf, + d->dirbuf_size); +} + +static XdpDir * +xdp_dir_new_physical (DIR *dir) +{ + XdpDir *d = g_new0 (XdpDir, 1); + d->dir = dir; + d->offset = 0; + d->entry = NULL; + return d; +} + +static XdpDir * +xdp_dir_new_buffered (fuse_req_t req) +{ + XdpDir *d = g_new0 (XdpDir, 1); + xdp_dir_add (d, req, ".", S_IFDIR); + xdp_dir_add (d, req, "..", S_IFDIR); + return d; +} + +static void +xdp_dir_add_docs (XdpDir *d, + fuse_req_t req, + const char *for_app_id) +{ + g_auto(GStrv) docs = NULL; + int i; + + docs = xdp_list_docs (); + for (i = 0; docs[i] != NULL; i++) + { + if (for_app_id) + { + g_autoptr(PermissionDbEntry) entry = xdp_lookup_doc (docs[i]); + if (entry == NULL || + !app_can_see_doc (entry, for_app_id)) + continue; + } + + xdp_dir_add (d, req, docs[i], S_IFDIR); + } +} + +static void +xdp_dir_add_apps (XdpDir *d, + XdpDomain *domain, + fuse_req_t req, + const char *for_app_id) +{ + g_auto(GStrv) apps = NULL; + g_auto(GStrv) names = NULL; + int i; + + /* First all pre-used apps as these can be created on demand */ + names = xdp_domain_get_inode_keys_as_string (domain); + for (i = 0; names[i] != NULL; i++) + xdp_dir_add (d, req, names[i], S_IFDIR); + + /* Then all in the db (that don't already have inodes) */ + apps = xdp_list_apps (); + for (i = 0; apps[i] != NULL; i++) + { + const char *app = apps[i]; + if (!g_strv_contains ((const gchar * const *)names, app)) + xdp_dir_add (d, req, app, S_IFDIR); + } +} + +static void +xdp_fuse_opendir (fuse_req_t req, + fuse_ino_t ino, + struct fuse_file_info *fi) +{ + g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino); + XdpDomain *domain = inode->domain; + XdpDir *d = NULL; + int open_flags = O_RDONLY | O_DIRECTORY; + DIR *dir; + const char *op = "OPENDIR"; + + g_debug ("OPENDIR %lx domain %d", ino, inode->domain->type); + + if (xdp_domain_is_virtual_type (domain)) + { + d = xdp_dir_new_buffered (req); + switch (domain->type) + { + case XDP_DOMAIN_ROOT: + xdp_dir_add (d, req, BY_APP_NAME, S_IFDIR); + xdp_dir_add_docs (d, req, NULL); + break; + case XDP_DOMAIN_APP: + xdp_dir_add_docs (d, req, domain->app_id); + break; + case XDP_DOMAIN_BY_APP: + xdp_dir_add_apps (d, inode->domain, req, NULL); + break; + default: + g_assert_not_reached (); + } + } + else + { + g_assert (domain->type == XDP_DOMAIN_DOCUMENT); + + if (xdp_document_domain_is_dir (domain)) + { + if (inode->physical) + { + int fd = openat (inode->physical->fd, ".", open_flags, 0); + if (fd < 0) + return xdp_reply_err (op, req, errno); + + dir = fdopendir (fd); + if (dir == NULL) + { + xdp_reply_err (op, req, errno); + close (fd); + return; + } + + d = xdp_dir_new_physical (dir); + } + else /* Nonphysical, i.e. toplevel */ + { + struct stat buf; + + d = xdp_dir_new_buffered (req); + + if (stat (domain->doc_path, &buf) == 0 && + buf.st_ino == domain->doc_dir_inode && + buf.st_dev == domain->doc_dir_device) + { + xdp_dir_add (d, req, domain->doc_file, buf.st_mode); + } + } + } + else + { + g_autofree char *main_path = g_build_filename (domain->doc_path, domain->doc_file, NULL); + struct stat buf; + GHashTableIter iter; + gpointer key, value; + + d = xdp_dir_new_buffered (req); + + if (stat (main_path, &buf) == 0) + xdp_dir_add (d, req, domain->doc_file, buf.st_mode); + + g_mutex_lock (&domain->tempfile_mutex); + + g_hash_table_iter_init (&iter, domain->tempfiles); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + const char *tempname = key; + xdp_dir_add (d, req, tempname, S_IFREG); + } + + g_mutex_unlock (&domain->tempfile_mutex); + } + } + + fi->fh = (gsize)d; + + if (fuse_reply_open (req, fi) == -ENOENT) + { + /* The opendir syscall was interrupted, so it must be cancelled */ + xdp_dir_free (d); + } +} + +static void +xdp_fuse_readdir (fuse_req_t req, + fuse_ino_t ino, + size_t size, + off_t off, + struct fuse_file_info *fi) +{ + XdpDir *d = (XdpDir *)fi->fh; + char *p; + size_t rem; + const char *op = "READDIR"; + + g_debug ("READDIR %lx %ld %ld", ino, size, off); + + if (d->dir) + { + g_autofree char *buf = g_try_malloc (size); + + if (buf == NULL) + { + xdp_reply_err (op, req, ENOMEM); + return; + } + + /* If offset is not same, need to seek it */ + if (off != d->offset) + { + seekdir (d->dir, off); + d->entry = NULL; + d->offset = off; + } + + p = buf; + rem = size; + while (TRUE) + { + size_t entsize; + off_t nextoff; + + if (!d->entry) + { + errno = 0; + d->entry = readdir (d->dir); + if (!d->entry) + { + if (errno && rem == size) + { + xdp_reply_err (op, req, errno); + return; + } + break; + } + } + nextoff = telldir (d->dir); + + struct stat st = { + .st_ino = FUSE_UNKNOWN_INO, + .st_mode = d->entry->d_type << 12, + }; + entsize = fuse_add_direntry (req, p, rem, + d->entry->d_name, &st, nextoff); + /* The above function returns the size of the entry size even though + * the copy failed due to smaller buf size, so I'm checking after this + * function and breaking out incase we exceed the size. + */ + if (entsize > rem) + break; + + p += entsize; + rem -= entsize; + + d->entry = NULL; + d->offset = nextoff; + } + + fuse_reply_buf(req, buf, size - rem); + } + else + { + if (off < d->dirbuf_size) + { + gsize reply_size = MIN (d->dirbuf_size - off, size); + g_autofree char *buf = g_memdup (d->dirbuf + off, reply_size); + fuse_reply_buf (req, buf, reply_size); + } + else + fuse_reply_buf (req, NULL, 0); + } +} + +static void +xdp_fuse_releasedir (fuse_req_t req, + fuse_ino_t ino, + struct fuse_file_info *fi) +{ + XdpDir *d = (XdpDir *)fi->fh; + const char *op = "RELEASEDIR"; + + g_debug ("RELEASEDIR %lx", ino); + + xdp_dir_free (d); + + xdp_reply_err (op, req, 0); +} + +static void +xdp_fuse_fsyncdir (fuse_req_t req, + fuse_ino_t ino, + int datasync, + struct fuse_file_info *fi) +{ + XdpDir *dir = (XdpDir *)fi->fh; + int fd, res; + const char *op = "FSYNCDIR"; + + g_debug ("FSYNCDIR %lx", ino); + + if (dir->dir) + { + fd = dirfd (dir->dir); + if (datasync) + res = fdatasync (fd); + else + res = fsync (fd); + } + else + res = 0; + + if (res == 0) + xdp_reply_err (op, req, 0); + else + xdp_reply_err (op, req, errno); +} + +static void +xdp_fuse_mkdir (fuse_req_t req, + fuse_ino_t parent_ino, + const char *name, + mode_t mode) +{ + g_autoptr(XdpInode) parent = xdp_inode_from_ino (parent_ino); + struct fuse_entry_param e; + int res; + xdp_autofd int close_fd = -1; + int dirfd; + const char *op = "MKDIR"; + + g_debug ("MKDIR %lx %s", parent_ino, name); + + if (!xdp_document_inode_checks (op, req, parent, + CHECK_CAN_WRITE | + CHECK_IS_DIRECTORY | + CHECK_IS_PHYSICAL)) + return; + + dirfd = xdp_document_inode_ensure_dirfd (parent, &close_fd); + if (dirfd < 0) + return xdp_reply_err (op, req, -dirfd); + + res = mkdirat (dirfd, name, mode); + if (res != 0) + return xdp_reply_err (op, req, errno); + + res = ensure_docdir_inode_by_name (parent, dirfd, name, &e); /* Takes ownershif of o_path_fd */ + if (res != 0) + return xdp_reply_err (op, req, -res); + + if (fuse_reply_entry (req, &e) == -ENOENT) + abort_reply_entry (&e); +} + +static void +xdp_fuse_unlink (fuse_req_t req, + fuse_ino_t parent_ino, + const char *filename) +{ + g_autoptr(XdpInode) parent = xdp_inode_from_ino (parent_ino); + XdpDomain *parent_domain = parent->domain; + int res = -1; + const char * op = "UNLINK"; + + g_debug ("UNLINK %lx %s", parent_ino, filename); + + if (!xdp_document_inode_checks (op, req, parent, + CHECK_CAN_WRITE | + CHECK_IS_PHYSICAL_IF_DIR)) + return; + + if (parent->physical) + { + res = unlinkat (parent->physical->fd, filename, 0); + if (res != 0) + return xdp_reply_err (op, req, errno); + } + else + { + xdp_autofd int dirfd = -1; + + /* Only reached for non-directory inodes */ + + dirfd = xdp_nonphysical_document_inode_opendir (parent); + if (dirfd < 0) + xdp_reply_err (op, req, -dirfd); + + if (strcmp (filename, parent_domain->doc_file) == 0) + { + res = unlinkat (dirfd, filename, 0); + if (res != 0) + return xdp_reply_err (op, req, errno); + } + else + { + gboolean removed = FALSE; + + /* Not directory and not main file, maybe a temporary file? */ + g_mutex_lock (&parent_domain->tempfile_mutex); + removed = g_hash_table_remove (parent_domain->tempfiles, filename); + g_mutex_unlock (&parent_domain->tempfile_mutex); + + if (!removed) + return xdp_reply_err (op, req, ENOENT); + } + } + + xdp_reply_err (op, req, 0); +} + +static int +try_renameat (int olddirfd, + const char *oldpath, + int newdirfd, + const char *newpath, + unsigned int flags) +{ +#if HAVE_RENAMEAT2 + return renameat2 (olddirfd, oldpath, newdirfd, newpath, flags); +#else + if (flags) + { + g_warning ("renameat2 is not supported by this system and rename flags are set"); + errno = EINVAL; + return -1; + } + + return renameat (olddirfd, oldpath, newdirfd, newpath); +#endif +} + + +static void +xdp_fuse_rename (fuse_req_t req, + fuse_ino_t parent_ino, + const char *name, + fuse_ino_t newparent_ino, + const char *newname, + unsigned int flags) +{ + g_autoptr(XdpInode) parent = xdp_inode_from_ino (parent_ino); + g_autoptr(XdpInode) newparent = xdp_inode_from_ino (newparent_ino); + g_autofree char *rename_flags_string = renameat2_flags_to_string (flags); + XdpDomain *domain; + int res, errsv; + int olddirfd, newdirfd, dirfd; + xdp_autofd int close_fd1 = -1; + xdp_autofd int close_fd2 = -1; + const char *op = "RENAME"; + + g_debug ("RENAME %lx %s -> %lx %s (flags: %s)", parent_ino, name, + newparent_ino, newname, rename_flags_string); + + if (!xdp_document_inode_checks (op, req, parent, + CHECK_CAN_WRITE | + CHECK_IS_PHYSICAL_IF_DIR)) + return; + + /* Don't allow cross-domain renames */ + if (parent->domain != newparent->domain) + return xdp_reply_err (op, req, EXDEV); + + domain = parent->domain; + if (xdp_document_domain_is_dir (domain)) + { + olddirfd = xdp_document_inode_ensure_dirfd (parent, &close_fd1); + if (olddirfd < 0) + return xdp_reply_err (op, req, -olddirfd); + + newdirfd = xdp_document_inode_ensure_dirfd (newparent, &close_fd2); + if (newdirfd < 0) + return xdp_reply_err (op, req, -newdirfd); + + res = try_renameat (olddirfd, name, newdirfd, newname, flags); + if (res != 0) + return xdp_reply_err (op, req, errno); + + xdp_reply_err (op, req, 0); + } + else + { + /* For non-directories, only allow renames in toplevel (nonphysical) dir */ + if (parent != newparent || parent->physical != NULL) + return xdp_reply_err (op, req, EACCES); + + /* Early exit for same file */ + if (strcmp (name, newname) == 0) + return xdp_reply_err (op, req, 0); + + dirfd = xdp_nonphysical_document_inode_opendir (parent); + if (dirfd < 0) + return xdp_reply_err (op, req, -dirfd); + close_fd1 = dirfd; + + if (strcmp (name, domain->doc_file) == 0) + { + /* Source is (maybe) main file, destination is tempfile */ + g_autofree char *tmpname = NULL; + int tmp_fd; + + /* Just use this to get an exclusive name, we will later replace its content */ + tmp_fd = open_temp_at (dirfd, newname, &tmpname, 0600); + if (tmp_fd < 0) + return xdp_reply_err (op, req, -tmp_fd); + close (tmp_fd); + + g_mutex_lock (&domain->tempfile_mutex); + res = try_renameat (dirfd, name, dirfd, tmpname, flags); + if (res == -1) + { + res = -errno; + /* Remove the temporary file if the move failed */ + (void) unlinkat (dirfd, tmpname, 0); + } + else + { + res = get_tempfile_for (parent, domain, newname, dirfd, tmpname, NULL); + } + + g_mutex_unlock (&domain->tempfile_mutex); + + if (res != 0) + return xdp_reply_err (op, req, -res); + + xdp_reply_err (op, req, 0); + } + else if (strcmp (newname, domain->doc_file) == 0) + { + gpointer stolen_value; + + /* source is (maybe) tempfile, Destination is main file */ + + g_mutex_lock (&domain->tempfile_mutex); + if (g_hash_table_steal_extended (domain->tempfiles, name, + NULL, &stolen_value)) + { + XdpTempfile *tempfile = stolen_value; + + res = try_renameat (dirfd, tempfile->tempname, dirfd, newname, flags); + errsv = errno; + + if (res == -1) /* Revert tempfile steal */ + g_hash_table_replace (domain->tempfiles, tempfile->name, tempfile); + else + { + /* Steal the old tempname so we don't unlink it */ + g_free (g_steal_pointer (&tempfile->tempname)); + xdp_tempfile_unref (tempfile); + } + } + else + { + res = -1; + errsv = ENOENT; + } + + g_mutex_unlock (&domain->tempfile_mutex); + + if (res != 0) + return xdp_reply_err (op, req, errsv); + + xdp_reply_err (op, req, 0); + } + else + { + /* Source and destinations are both tempfiles, no need to change anything on disk */ + gboolean found_tempfile = FALSE; + gpointer stolen_value; + + /* Renaming temp file to temp file */ + g_mutex_lock (&domain->tempfile_mutex); + if (g_hash_table_steal_extended (domain->tempfiles, name, + NULL, &stolen_value)) + { + XdpTempfile *tempfile = stolen_value; + + found_tempfile = TRUE; + + g_free (tempfile->name); + tempfile->name = g_strdup (newname); + + /* This destroys any pre-existing tempfile with this name */ + g_hash_table_replace (domain->tempfiles, tempfile->name, tempfile); + } + g_mutex_unlock (&domain->tempfile_mutex); + + if (!found_tempfile) + return xdp_reply_err (op, req, ENOENT); + + xdp_reply_err (op, req, 0); + } + } +} + +static void +xdp_fuse_access (fuse_req_t req, + fuse_ino_t ino, + int mask) +{ + g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino); + g_autofree char *path = NULL; + int res; + const char *op = "ACCESS"; + + g_debug ("ACCESS %lx", ino); + + if (inode->domain->type != XDP_DOMAIN_DOCUMENT) + { + if (mask & W_OK) + xdp_reply_err (op, req, EPERM); + else + xdp_reply_err (op, req, 0); + + return; + } + + if ((mask & W_OK) != 0 && + !xdp_document_domain_can_write (inode->domain)) + return xdp_reply_err (op, req, EPERM); + + if (inode->physical) + { + path = fd_to_path (inode->physical->fd); + res = access (path, mask); + } + else + { + if (xdp_document_domain_is_dir (inode->domain)) + { + if (mask & W_OK) + res = EPERM; + else + res = 0; + } + else + res = access (inode->domain->doc_path, mask); + } + + if (res == -1) + xdp_reply_err (op, req, errno); + else + xdp_reply_err (op, req, 0); +} + +static void +xdp_fuse_rmdir (fuse_req_t req, + fuse_ino_t parent_ino, + const char *filename) +{ + g_autoptr(XdpInode) parent = xdp_inode_from_ino (parent_ino); + xdp_autofd int close_fd = -1; + int dirfd; + int res; + const char *op = "RMDIR"; + + g_debug ("RMDIR %lx %s", parent_ino, filename); + + if (!xdp_document_inode_checks (op, req, parent, + CHECK_CAN_WRITE | + CHECK_IS_DIRECTORY | + CHECK_IS_PHYSICAL)) + return; + + dirfd = xdp_document_inode_ensure_dirfd (parent, &close_fd); + if (dirfd < 0) + return xdp_reply_err (op, req, -dirfd); + + res = unlinkat (dirfd, filename, AT_REMOVEDIR); + if (res != 0) + xdp_reply_err (op, req, errno); + + xdp_reply_err (op, req, 0); +} + +static void +xdp_fuse_readlink (fuse_req_t req, + fuse_ino_t ino) +{ + g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino); + char linkname[PATH_MAX + 1]; + ssize_t res; + const char *op = "READLINK"; + + g_debug ("READLINK %lx", ino); + + if (!xdp_document_inode_checks (op, req, inode, + CHECK_IS_DIRECTORY | + CHECK_IS_PHYSICAL)) + return; + + if (inode->physical == NULL) + return xdp_reply_err (op, req, EINVAL); + + res = readlinkat (inode->physical->fd, "", linkname, sizeof(linkname)); + if (res < 0) + return xdp_reply_err (op, req, errno); + + linkname[res] = '\0'; + fuse_reply_readlink (req, linkname); +} + +static void +xdp_fuse_symlink (fuse_req_t req, + const char *link, + fuse_ino_t parent_ino, + const char *name) +{ + g_autoptr(XdpInode) parent = xdp_inode_from_ino (parent_ino); + int res; + int dirfd; + xdp_autofd int close_fd = -1; + struct fuse_entry_param e; + const char * op = "SYMLINK"; + + g_debug ("SYMLINK %s %lx %s", link, parent_ino, name); + + if (!xdp_document_inode_checks (op, req, parent, + CHECK_CAN_WRITE | + CHECK_IS_DIRECTORY | + CHECK_IS_PHYSICAL)) + return; + + dirfd = xdp_document_inode_ensure_dirfd (parent, &close_fd); + if (dirfd < 0) + return xdp_reply_err (op, req, -dirfd); + + res = symlinkat (link, dirfd, name); + if (res != 0) + return xdp_reply_err (op, req, errno); + + res = ensure_docdir_inode_by_name (parent, dirfd, name, &e); /* Takes ownershif of o_path_fd */ + if (res != 0) + return xdp_reply_err (op, req, -res); + + if (fuse_reply_entry (req, &e) == -ENOENT) + abort_reply_entry (&e); +} + +static void +xdp_fuse_link (fuse_req_t req, + fuse_ino_t ino, + fuse_ino_t newparent_ino, + const char *newname) +{ + g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino); + g_autoptr(XdpInode) newparent = xdp_inode_from_ino (newparent_ino); + int res; + g_autofree char *proc_path = NULL; + int newparent_dirfd; + xdp_autofd int close_fd = -1; + struct fuse_entry_param e; + const char * op = "LINK"; + + g_debug ("LINK %lx %lx %s", ino, newparent_ino, newname); + + /* hardlinks only supported in docdirs, and only physical files */ + if (!xdp_document_inode_checks (op, req, inode, + CHECK_CAN_WRITE | + CHECK_IS_DIRECTORY | + CHECK_IS_PHYSICAL)) + return; + + /* Don't allow linking between domains */ + if (inode->domain != newparent->domain) + return xdp_reply_err (op, req, EXDEV); + + proc_path = fd_to_path (inode->physical->fd); + newparent_dirfd = xdp_document_inode_ensure_dirfd (newparent, &close_fd); + if (newparent_dirfd < 0) + return xdp_reply_err (op, req, -newparent_dirfd); + + res = linkat (AT_FDCWD, proc_path, newparent_dirfd, newname, AT_SYMLINK_FOLLOW); + if (res != 0) + return xdp_reply_err (op, req, errno); + + res = ensure_docdir_inode_by_name (inode, newparent_dirfd, newname, &e); /* Takes ownership of o_path_fd */ + if (res != 0) + return xdp_reply_err (op, req, -res); + + if (fuse_reply_entry (req, &e) == -ENOENT) + abort_reply_entry (&e); +} + + +static void +xdp_fuse_statfs (fuse_req_t req, + fuse_ino_t ino) +{ + g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino); + struct statvfs buf; + int res; + const char *op = "STATFS"; + + g_debug ("STATFS %lx", ino); + + if (!xdp_document_inode_checks (op, req, inode, 0)) + return; + + if (inode->physical) + res = fstatvfs (inode->physical->fd, &buf); + else + res = statvfs (inode->domain->doc_path, &buf); + + if (!res) + fuse_reply_statfs (req, &buf); + else + xdp_reply_err (op, req, errno); +} + +static void +xdp_fuse_setxattr (fuse_req_t req, + fuse_ino_t ino, + const char *name, + const char *value, + size_t size, + int flags) +{ + g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino); + ssize_t res; + g_autofree char *path = NULL; + const char *op = "SETXATTR"; + + g_debug ("SETXATTR %lx %s", ino, name); + + if (!xdp_document_inode_checks (op, req, inode, + CHECK_CAN_WRITE | + CHECK_IS_DIRECTORY | + CHECK_IS_PHYSICAL)) + return; + + path = fd_to_path (inode->physical->fd); +#if defined(HAVE_SYS_XATTR_H) + res = setxattr (path, name, value, size, flags); +#elif defined(HAVE_SYS_EXTATTR_H) + res = extattr_set_file (path, EXTATTR_NAMESPACE_USER, name, value, size); +#else +#error "Not implemented for your platform" +#endif + + if (res < 0) + return xdp_reply_err (op, req, errno); + + xdp_reply_err (op, req, 0); +} + +static void +xdp_fuse_getxattr (fuse_req_t req, + fuse_ino_t ino, + const char *name, + size_t size) +{ + g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino); + ssize_t res; + g_autofree char *buf = NULL; + g_autofree char *path = NULL; + const char *op = "GETXATTR"; + + g_debug ("GETXATTR %lx %s %ld", ino, name, size); + + if (inode->domain->type != XDP_DOMAIN_DOCUMENT) + return xdp_reply_err (op, req, ENODATA); + + if (size != 0) + buf = g_malloc (size); + + path = xdp_document_inode_get_self_as_path (inode); + if (path == NULL) + res = ENODATA; + else + { +#if defined(HAVE_SYS_XATTR_H) + res = getxattr (path, name, buf, size); +#elif defined(HAVE_SYS_EXTATTR_H) + res = extattr_get_file (path, EXTATTR_NAMESPACE_USER, name, buf, size); +#else +#error "Not implemented for your platform" +#endif + } + if (res < 0) + return xdp_reply_err (op, req, errno); + + if (size == 0) + fuse_reply_xattr (req, res); + else + fuse_reply_buf (req, buf, res); +} + +static void +xdp_fuse_listxattr (fuse_req_t req, + fuse_ino_t ino, + size_t size) +{ + g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino); + ssize_t res; + g_autofree char *buf = NULL; + g_autofree char *path = NULL; + const char *op = "LISTXATTR"; + + g_debug ("LISTXATTR %lx %ld", ino, size); + + if (inode->domain->type != XDP_DOMAIN_DOCUMENT) + return xdp_reply_err (op, req, ENOTSUP); + + if (size != 0) + buf = g_malloc (size); + + path = xdp_document_inode_get_self_as_path (inode); + if (path) + { +#if defined(HAVE_SYS_XATTR_H) + res = listxattr (path, buf, size); +#elif defined(HAVE_SYS_EXTATTR_H) + res = extattr_list_file (path, EXTATTR_NAMESPACE_USER, buf, size); +#else +#error "Not implemented for your platform" +#endif + } + else + res = 0; + + if (res < 0) + return xdp_reply_err (op, req, errno); + + if (size == 0) + fuse_reply_xattr (req, res); + else + fuse_reply_buf (req, buf, res); +} + +static void +xdp_fuse_removexattr (fuse_req_t req, + fuse_ino_t ino, + const char *name) +{ + g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino); + g_autofree char *path = NULL; + ssize_t res; + const char *op = "REMOVEXATTR"; + + g_debug ("REMOVEXATTR %lx %s", ino, name); + + if (!xdp_document_inode_checks (op, req, inode, + CHECK_CAN_WRITE | + CHECK_IS_DIRECTORY | + CHECK_IS_PHYSICAL)) + return; + + path = fd_to_path (inode->physical->fd); +#if defined(HAVE_SYS_XATTR_H) + res = removexattr (path, name); +#elif defined(HAVE_SYS_EXTATTR_H) + res = extattr_delete_file (path, EXTATTR_NAMESPACE_USER, name); +#else +#error "Not implemented for your platform" +#endif + + if (res < 0) + xdp_reply_err (op, req, errno); + else + xdp_reply_err (op, req, 0); +} + +static void +xdp_fuse_getlk (fuse_req_t req, + fuse_ino_t ino, + struct fuse_file_info *fi, + struct flock *lock) +{ + const char *op = "GETLK"; + + g_debug ("GETLK %lx", ino); + + xdp_reply_err (op, req, ENOSYS); +} + +static void +xdp_fuse_setlk (fuse_req_t req, + fuse_ino_t ino, + struct fuse_file_info *fi, + struct flock *lock, + int sleep) +{ + const char *op = "SETLK"; + + g_debug ("SETLK %lx", ino); + + xdp_reply_err (op, req, ENOSYS); +} + +static void +xdp_fuse_flock (fuse_req_t req, + fuse_ino_t ino, + struct fuse_file_info *fi, + int lock_op) +{ + const char *op = "FLOCK"; + + g_debug ("FLOCK %lx", ino); + + xdp_reply_err (op, req, ENOSYS); +} + +static void +xdp_fuse_init_cb (void *userdata, + struct fuse_conn_info *conn) +{ + g_debug ("INIT"); + + /* splice_read: use splice() to read from fuse pipe */ + conn->want |= FUSE_CAP_SPLICE_READ; + /* splice_write: use splice() to write to fuse pipe */ + conn->want |= FUSE_CAP_SPLICE_WRITE; + /* splice_move: move buffers from writing app to kernel during splice write */ + conn->want |= FUSE_CAP_SPLICE_MOVE; + /* atomic_o_trunc: We handle O_TRUNC in create() */ + conn->want |= FUSE_CAP_ATOMIC_O_TRUNC; +} + +extern gboolean on_fuse_unmount (void *); + +static void +xdp_fuse_destroy_cb (void *userdata) +{ + g_debug ("DESTROY"); + + /* Ensure we call this on the main thread */ + g_idle_add ((GSourceFunc) on_fuse_unmount, NULL); +} + +static struct fuse_lowlevel_ops xdp_fuse_oper = { + .init = xdp_fuse_init_cb, + .destroy = xdp_fuse_destroy_cb, + .lookup = xdp_fuse_lookup, + .getattr = xdp_fuse_getattr, + .setattr = xdp_fuse_setattr, + .readdir = xdp_fuse_readdir, + .open = xdp_fuse_open, + .read = xdp_fuse_read, + .write = xdp_fuse_write, + .write_buf = xdp_fuse_write_buf, + .fsync = xdp_fuse_fsync, + .forget = xdp_fuse_forget, + .forget_multi = xdp_fuse_forget_multi, + .releasedir = xdp_fuse_releasedir, + .release = xdp_fuse_release, + .opendir = xdp_fuse_opendir, + .fsyncdir = xdp_fuse_fsyncdir, + .create = xdp_fuse_create, + .unlink = xdp_fuse_unlink, + .rename = xdp_fuse_rename, + .access = xdp_fuse_access, + .readlink = xdp_fuse_readlink, + .rmdir = xdp_fuse_rmdir, + .mkdir = xdp_fuse_mkdir, + .symlink = xdp_fuse_symlink, + .link = xdp_fuse_link, + .flush = xdp_fuse_flush, + .statfs = xdp_fuse_statfs, + .setxattr = xdp_fuse_setxattr, + .getxattr = xdp_fuse_getxattr, + .listxattr = xdp_fuse_listxattr, + .removexattr = xdp_fuse_removexattr, + .getlk = xdp_fuse_getlk, + .setlk = xdp_fuse_setlk, + .flock = xdp_fuse_flock, + .fallocate = xdp_fuse_fallocate, +}; + +typedef struct { + GMutex lock; + GCond cond; + GError *error; +} XdpFuseThreadData; + +static void +xdp_fuse_mainloop (struct fuse_session *se, + struct fuse_loop_config *loop_config) +{ + const char *status; + + fuse_session_loop_mt (se, loop_config); + + status = getenv ("TEST_DOCUMENT_PORTAL_FUSE_STATUS"); + if (status) + { + GError *error = NULL; + g_autoptr(GString) s = g_string_new (""); + + g_string_append (s, "ok"); + + g_file_set_contents (status, s->str, -1, &error); + g_assert_no_error (error); + } +} + +typedef struct fuse_args XdpAutoFuseArgs; +G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC (XdpAutoFuseArgs, fuse_opt_free_args); + +static gpointer +xdp_fuse_thread (gpointer data) +{ + /* Options: + * auto_unmount: Tell fusermount to auto unmount if we die. + */ + static char *fusermount_argv[] = { + "xdp-fuse", "-osubtype=portal,fsname=portal,auto_unmount", + }; + g_auto(XdpAutoFuseArgs) args = + FUSE_ARGS_INIT (G_N_ELEMENTS (fusermount_argv), fusermount_argv); + g_autoptr(GMutexLocker) locker = NULL; + g_autoptr(GMutexLocker) session_locker = NULL; + const char *path; + struct fuse_session *se; + XdpFuseThreadData *thread_data = data; + struct fuse_cmdline_opts opts = {0}; + struct fuse_loop_config loop_config = {0}; + + locker = g_mutex_locker_new (&thread_data->lock); + fuse_pthread = pthread_self (); + + g_cond_signal (&thread_data->cond); + + if (fuse_parse_cmdline (&args, &opts) != 0) + { + g_set_error (&thread_data->error, XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Impossible to parse command line"); + return NULL; + } + + se = fuse_session_new (&args, &xdp_fuse_oper, + sizeof (xdp_fuse_oper), NULL); + if (se == NULL) + { + g_set_error (&thread_data->error, XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_FAILED, + "Can't create fuse session"); + return NULL; + } + + path = xdp_fuse_get_mountpoint (); + if (fuse_session_mount (se, path) != 0) + { + fuse_session_destroy (se); + g_set_error (&thread_data->error, XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_FAILED, + "Can't mount path %s", path); + return NULL; + } + + session = se; + thread_data = NULL; + g_clear_pointer (&locker, g_mutex_locker_free); + + loop_config.clone_fd = opts.clone_fd; + loop_config.max_idle_threads = opts.max_idle_threads; + thread_data = NULL; + + session_locker = g_mutex_locker_new (&G_LOCK_NAME (session)); + g_clear_pointer (&session_locker, g_mutex_locker_free); + xdp_fuse_mainloop (session, &loop_config); + + session_locker = g_mutex_locker_new (&G_LOCK_NAME (session)); + fuse_session_unmount (se); + fuse_session_destroy (se); + session = NULL; + + return NULL; +} + +gboolean +xdp_fuse_init (GError **error) +{ + XdpFuseThreadData thread_data = {0}; + struct stat st; + struct statfs stfs; + const char *path; + struct rlimit rl; + int statfs_res; + g_autoptr(XdpDomain) root_domain = NULL; + g_autoptr(XdpDomain) by_app_domain = NULL; + + my_uid = getuid (); + my_gid = getgid (); + + all_inodes = g_hash_table_new_full (g_int64_hash, g_int64_equal, NULL, NULL); + + root_domain = xdp_domain_new_root (); + root_inode = xdp_inode_new (root_domain, NULL); + by_app_domain = xdp_domain_new_by_app (root_inode); + by_app_inode = xdp_inode_new (by_app_domain, NULL); + + physical_inodes = + g_hash_table_new_full (devino_hash, devino_equal, NULL, NULL); + + /* Bump nr of filedescriptor limit to max */ + if (getrlimit (RLIMIT_NOFILE , &rl) == 0 && + rl.rlim_cur != rl.rlim_max) + { + rl.rlim_cur = rl.rlim_max; + setrlimit (RLIMIT_NOFILE, &rl); + } + + path = xdp_fuse_get_mountpoint (); + + if ((stat (path, &st) == -1 && errno == ENOTCONN) || + (((statfs_res = statfs (path, &stfs)) == -1 && errno == ENOTCONN) || + (statfs_res == 0 && stfs.f_type == 0x65735546 /* fuse */))) + { + int count; + char *umount_argv[] = { "fusermount3", "-u", "-z", (char *) path, NULL }; + + g_spawn_sync (NULL, umount_argv, NULL, G_SPAWN_SEARCH_PATH, + NULL, NULL, NULL, NULL, NULL, NULL); + + g_usleep (10000); /* 10ms */ + count = 0; + while (stat (path, &st) == -1 && count < 10) + g_usleep (10000); /* 10ms */ + } + + if (g_mkdir_with_parents (path, 0700)) + { + g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_FAILED, + "Unable to create dir %s", path); + return FALSE; + } + + g_mutex_init (&thread_data.lock); + g_cond_init (&thread_data.cond); + + g_mutex_lock (&thread_data.lock); + XDP_AUTOLOCK (session); + fuse_thread = g_thread_new ("fuse mainloop", xdp_fuse_thread, &thread_data); + + while (session == NULL && thread_data.error == NULL) + g_cond_wait (&thread_data.cond, &thread_data.lock); + + g_mutex_unlock (&thread_data.lock); + g_cond_clear (&thread_data.cond); + g_mutex_clear (&thread_data.lock); + + if (thread_data.error != NULL) + { + g_propagate_error (error, g_steal_pointer (&thread_data.error)); + return FALSE; + } + + g_assert (session != NULL); + + return TRUE; +} + +void +xdp_fuse_exit (void) +{ + { + XDP_AUTOLOCK (session); + + if (session) + fuse_session_exit (session); + + if (fuse_pthread) + pthread_kill (fuse_pthread, SIGHUP); + } + + g_clear_pointer (&fuse_thread, g_thread_join); + g_assert (session == NULL); +} + +const char * +xdp_fuse_get_mountpoint (void) +{ + if (mount_path == NULL) + mount_path = g_build_filename (g_get_user_runtime_dir (), "doc", NULL); + return mount_path; +} + +typedef struct { + fuse_ino_t ino; + char *filename; +} Invalidate; + +/* Called with domain_inodes lock held, don't block */ +static void +invalidate_doc_inode (XdpInode *parent_inode, + const char *doc_id, + GArray *invalidates) +{ + XdpInode *doc_inode = g_hash_table_lookup (parent_inode->domain->inodes, doc_id); + Invalidate inval; + + if (doc_inode == NULL) + return; + + inval.ino = xdp_inode_to_ino (doc_inode); + inval.filename = NULL; + g_array_append_val (invalidates, inval); + + inval.ino = xdp_inode_to_ino (parent_inode); + inval.filename = g_strdup (doc_id); + g_array_append_val (invalidates, inval); + + /* No need to invalidate doc children, we don't cache them */ +} + + +/* Called when a apps permissions to see a document is changed, + and with null opt_app_id when the doc is created/removed */ +void +xdp_fuse_invalidate_doc_app (const char *doc_id, + const char *opt_app_id) +{ + g_autoptr(GArray) invalidates = NULL; + XDP_AUTOLOCK (session); + int i; + + /* This can happen if fuse is not initialized yet for the very + first dbus message that activated the service */ + if (session == NULL) + return; + + g_debug ("invalidate %s/%s", doc_id, opt_app_id ? opt_app_id : "*"); + + invalidates = g_array_new (FALSE, FALSE, sizeof (Invalidate)); + + G_LOCK (domain_inodes); + if (opt_app_id != NULL) + { + XdpInode *app_inode = g_hash_table_lookup (by_app_inode->domain->inodes, opt_app_id); + if (app_inode) + invalidate_doc_inode (app_inode, doc_id, invalidates); + } + else + { + GHashTableIter iter; + gpointer key, value; + + invalidate_doc_inode (root_inode, doc_id, invalidates); + g_hash_table_iter_init (&iter, by_app_inode->domain->inodes); + while (g_hash_table_iter_next (&iter, &key, &value)) + invalidate_doc_inode ((XdpInode *)value, doc_id, invalidates); + } + + G_UNLOCK (domain_inodes); + + for (i = 0; i < invalidates->len; i++) + { + Invalidate *invalidate = &g_array_index (invalidates, Invalidate, i); + + if (invalidate->filename) + { + fuse_lowlevel_notify_inval_entry (session, invalidate->ino, + invalidate->filename, strlen (invalidate->filename)); + g_free (invalidate->filename); + } + else + fuse_lowlevel_notify_inval_inode (session, invalidate->ino, 0, 0); + } +} + +char * +xdp_fuse_lookup_id_for_inode (ino_t ino, gboolean directory, + char **real_path_out) +{ + g_autoptr(XdpDomain) domain = NULL; + g_autoptr(XdpPhysicalInode) physical = NULL; + DevIno file_devino; + struct stat buf; + + if (real_path_out) + *real_path_out = NULL; + + G_LOCK (all_inodes); + { + XdpInode *inode = g_hash_table_lookup (all_inodes, &ino); + if (inode) + { + /* We're not allowed to ressurect the inode here, but we can get the data while in the lock */ + domain = xdp_domain_ref (inode->domain); + if (inode->physical) + physical = xdp_physical_inode_ref (inode->physical); + } + } + G_UNLOCK (all_inodes); + + if (domain == NULL) + return NULL; + + if (domain->type != XDP_DOMAIN_DOCUMENT) + return NULL; + + if (physical == NULL) + return NULL; + + file_devino = physical->backing_devino; + + if (!xdp_document_domain_is_dir (domain)) + { + g_autofree char *main_path = g_build_filename (domain->doc_path, domain->doc_file, NULL); + + /* file document */ + + if (directory) + return NULL; + + /* Only return for main file */ + if (lstat (main_path, &buf) == 0 && + buf.st_dev == file_devino.dev && + buf.st_ino == file_devino.ino) + return g_strdup (domain->doc_id); + } + else + { + /* directory document */ + + /* Only return entire doc for main dir */ + if (file_devino.dev == domain->doc_dir_device && + file_devino.ino == domain->doc_dir_inode) + return g_strdup (domain->doc_id); + + /* But maybe its a subfile of the document */ + if (real_path_out) + { + g_autofree char *fd_path = fd_to_path (physical->fd); + char path_buffer[PATH_MAX + 1]; + DevIno file_devino = physical->backing_devino; + ssize_t symlink_size; + struct stat buf; + + /* Try to extract a real path to the file (and verify it goes to the same place as the fd) */ + symlink_size = readlink (fd_path, path_buffer, PATH_MAX); + if (symlink_size >= 1) + { + path_buffer[symlink_size] = 0; + + if (lstat (path_buffer, &buf) == 0 && + buf.st_dev == file_devino.dev && + buf.st_ino == file_devino.ino) + { + *real_path_out = g_strdup (path_buffer); + return g_strdup (domain->doc_id); + } + } + } + } + + return NULL; +} diff --git a/document-portal/document-portal-fuse.h b/document-portal/document-portal-fuse.h new file mode 100644 index 0000000..fa4ad45 --- /dev/null +++ b/document-portal/document-portal-fuse.h @@ -0,0 +1,25 @@ +#ifndef XDP_FUSE_H +#define XDP_FUSE_H + +#include +#include "permission-db.h" + +G_BEGIN_DECLS + +char ** xdp_list_apps (void); +char ** xdp_list_docs (void); +PermissionDbEntry *xdp_lookup_doc (const char *doc_id); + +gboolean xdp_fuse_init (GError **error); +void xdp_fuse_exit (void); +const char *xdp_fuse_get_mountpoint (void); +void xdp_fuse_invalidate_doc_app (const char *doc_id, + const char *opt_app_id); +char *xdp_fuse_lookup_id_for_inode (ino_t inode, + gboolean directory, + char **real_path_out); + + +G_END_DECLS + +#endif /* XDP_FUSE_H */ diff --git a/document-portal/document-portal.c b/document-portal/document-portal.c new file mode 100644 index 0000000..97765da --- /dev/null +++ b/document-portal/document-portal.c @@ -0,0 +1,1701 @@ +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include "glib-backports.h" +#include "document-portal-dbus.h" +#include "document-store.h" +#include "src/xdp-utils.h" +#include "permission-db.h" +#include "permission-store-dbus.h" +#include "document-portal-fuse.h" +#include "file-transfer.h" +#include "document-portal.h" + +#define TABLE_NAME "documents" + +typedef struct +{ + char *doc_id; + int fd; + char *owner; + guint flags; + + GDBusMethodInvocation *finish_invocation; +} XdpDocUpdate; + + +static GMainLoop *loop = NULL; +static PermissionDb *db = NULL; +static XdgPermissionStore *permission_store; +static int final_exit_status = 0; +static GError *exit_error = NULL; +static dev_t fuse_dev = 0; +static GQueue get_mount_point_invocations = G_QUEUE_INIT; +static XdpDbusDocuments *dbus_api; + +G_LOCK_DEFINE (db); + +char ** +xdp_list_apps (void) +{ + XDP_AUTOLOCK (db); + return permission_db_list_apps (db); +} + +char ** +xdp_list_docs (void) +{ + XDP_AUTOLOCK (db); + return permission_db_list_ids (db); +} + +PermissionDbEntry * +xdp_lookup_doc (const char *doc_id) +{ + XDP_AUTOLOCK (db); + return permission_db_lookup (db, doc_id); +} + +static gboolean +persist_entry (PermissionDbEntry *entry) +{ + guint32 flags = document_entry_get_flags (entry); + + return (flags & DOCUMENT_ENTRY_FLAG_TRANSIENT) == 0; +} + +static void +do_set_permissions (PermissionDbEntry *entry, + const char *doc_id, + const char *app_id, + DocumentPermissionFlags perms) +{ + g_autofree const char **perms_s = xdg_unparse_permissions (perms); + + g_autoptr(PermissionDbEntry) new_entry = NULL; + + g_debug ("set_permissions %s %s %x", doc_id, app_id, perms); + + new_entry = permission_db_entry_set_app_permissions (entry, app_id, perms_s); + permission_db_set_entry (db, doc_id, new_entry); + + if (persist_entry (new_entry)) + { + xdg_permission_store_call_set_permission (permission_store, + TABLE_NAME, + FALSE, + doc_id, + app_id, + perms_s, + NULL, + NULL, NULL); + } +} + +static void +portal_grant_permissions (GDBusMethodInvocation *invocation, + GVariant *parameters, + XdpAppInfo *app_info) +{ + const char *target_app_id; + const char *id; + g_autofree const char **permissions = NULL; + DocumentPermissionFlags perms; + GError *error = NULL; + + g_autoptr(PermissionDbEntry) entry = NULL; + + g_variant_get (parameters, "(&s&s^a&s)", &id, &target_app_id, &permissions); + + { + XDP_AUTOLOCK (db); + + entry = permission_db_lookup (db, id); + if (entry == NULL) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_FOUND, + "No such document: %s", id); + return; + } + + if (!xdp_is_valid_app_id (target_app_id)) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "'%s' is not a valid app name", target_app_id); + return; + } + + perms = xdp_parse_permissions (permissions, &error); + if (error) + { + g_dbus_method_invocation_take_error (invocation, error); + return; + } + + /* Must have grant-permissions and all the newly granted permissions */ + if (!document_entry_has_permissions (entry, app_info, + DOCUMENT_PERMISSION_FLAGS_GRANT_PERMISSIONS | perms)) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Not enough permissions"); + return; + } + + do_set_permissions (entry, id, target_app_id, + perms | document_entry_get_permissions_by_app_id (entry, target_app_id)); + } + + /* Invalidate with lock dropped to avoid deadlock */ + xdp_fuse_invalidate_doc_app (id, target_app_id); + + g_dbus_method_invocation_return_value (invocation, g_variant_new ("()")); +} + +static void +portal_revoke_permissions (GDBusMethodInvocation *invocation, + GVariant *parameters, + XdpAppInfo *app_info) +{ + const char *app_id = xdp_app_info_get_id (app_info); + const char *target_app_id; + const char *id; + g_autofree const char **permissions = NULL; + GError *error = NULL; + + g_autoptr(PermissionDbEntry) entry = NULL; + DocumentPermissionFlags perms; + + g_variant_get (parameters, "(&s&s^a&s)", &id, &target_app_id, &permissions); + + { + XDP_AUTOLOCK (db); + + entry = permission_db_lookup (db, id); + if (entry == NULL) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_FOUND, + "No such document: %s", id); + return; + } + + if (!xdp_is_valid_app_id (target_app_id)) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "'%s' is not a valid app name", target_app_id); + return; + } + + perms = xdp_parse_permissions (permissions, &error); + if (error) + { + g_dbus_method_invocation_take_error (invocation, error); + return; + } + + /* Must have grant-permissions, or be itself */ + if (!document_entry_has_permissions (entry, app_info, + DOCUMENT_PERMISSION_FLAGS_GRANT_PERMISSIONS) || + strcmp (app_id, target_app_id) == 0) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Not enough permissions"); + return; + } + + do_set_permissions (entry, id, target_app_id, + ~perms & document_entry_get_permissions_by_app_id (entry, target_app_id)); + } + + /* Invalidate with lock dropped to avoid deadlock */ + xdp_fuse_invalidate_doc_app (id, target_app_id); + + g_dbus_method_invocation_return_value (invocation, g_variant_new ("()")); +} + +static void +portal_delete (GDBusMethodInvocation *invocation, + GVariant *parameters, + XdpAppInfo *app_info) +{ + const char *id; + g_autoptr(PermissionDbEntry) entry = NULL; + g_autofree const char **old_apps = NULL; + int i; + + g_variant_get (parameters, "(s)", &id); + + g_debug ("portal_delete %s", id); + + { + XDP_AUTOLOCK (db); + + entry = permission_db_lookup (db, id); + if (entry == NULL) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_FOUND, + "No such document: %s", id); + return; + } + + if (!document_entry_has_permissions (entry, app_info, DOCUMENT_PERMISSION_FLAGS_DELETE)) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Not enough permissions"); + return; + } + + g_debug ("delete %s", id); + + permission_db_set_entry (db, id, NULL); + + if (persist_entry (entry)) + xdg_permission_store_call_delete (permission_store, TABLE_NAME, + id, NULL, NULL, NULL); + } + + /* All i/o is done now, so drop the lock so we can invalidate the fuse caches */ + old_apps = permission_db_entry_list_apps (entry); + for (i = 0; old_apps[i] != NULL; i++) + xdp_fuse_invalidate_doc_app (id, old_apps[i]); + xdp_fuse_invalidate_doc_app (id, NULL); + + /* Now fuse view is up-to-date, so we can return the call */ + g_dbus_method_invocation_return_value (invocation, g_variant_new ("()")); +} + +static char * +do_create_doc (struct stat *parent_st_buf, const char *path, gboolean reuse_existing, gboolean persistent, gboolean directory) +{ + g_autoptr(GVariant) data = NULL; + g_autoptr(PermissionDbEntry) entry = NULL; + g_auto(GStrv) ids = NULL; + char *id = NULL; + guint32 flags = 0; + + g_debug ("Creating document at path '%s', resuse_existing: %d, persistent: %d, directory: %d", path, reuse_existing, persistent, directory); + + if (!reuse_existing) + flags |= DOCUMENT_ENTRY_FLAG_UNIQUE; + if (!persistent) + flags |= DOCUMENT_ENTRY_FLAG_TRANSIENT; + if (directory) + flags |= DOCUMENT_ENTRY_FLAG_DIRECTORY; + data = + g_variant_ref_sink (g_variant_new ("(^ayttu)", + path, + (guint64) parent_st_buf->st_dev, + (guint64) parent_st_buf->st_ino, + flags)); + + if (reuse_existing) + { + ids = permission_db_list_ids_by_value (db, data); + + if (ids[0] != NULL) + return g_strdup (ids[0]); /* Reuse pre-existing entry with same path */ + } + + while (TRUE) + { + g_autoptr(PermissionDbEntry) existing = NULL; + + g_clear_pointer (&id, g_free); + id = xdp_name_from_id ((guint32) g_random_int ()); + existing = permission_db_lookup (db, id); + if (existing == NULL) + break; + } + + g_debug ("create_doc %s", id); + + entry = permission_db_entry_new (data); + permission_db_set_entry (db, id, entry); + + if (persistent) + { + xdg_permission_store_call_set (permission_store, + TABLE_NAME, + TRUE, + id, + g_variant_new_array (G_VARIANT_TYPE ("{sas}"), NULL, 0), + g_variant_new_variant (data), + NULL, NULL, NULL); + } + + return id; +} + +gboolean +validate_fd (int fd, + XdpAppInfo *app_info, + ValidateFdType ensure_type, + struct stat *st_buf, + struct stat *real_dir_st_buf, + char **path_out, + gboolean *writable_out, + GError **error) +{ + g_autofree char *path = NULL; + g_autofree char *dirname = NULL; + g_autofree char *name = NULL; + xdp_autofd int dir_fd = -1; + struct stat real_st_buf; + g_autoptr(GError) local_error = NULL; + + path = xdp_app_info_get_path_for_fd (app_info, fd, 0, st_buf, writable_out, &local_error); + + if (path == NULL) + { + g_debug ("Invalid fd passed: %s", local_error->message); + goto errout; + } + + if ((ensure_type == VALIDATE_FD_FILE_TYPE_REGULAR || ensure_type == VALIDATE_FD_FILE_TYPE_ANY) && S_ISREG (st_buf->st_mode)) + { + /* We open the parent directory and do the stat in that, so that we have + * trustworthy parent dev/ino + filename for later verification. Otherwise the caller + * could later replace a parent with a symlink and make us read some other file. + */ + dirname = g_path_get_dirname (path); + name = g_path_get_basename (path); + } + else if ((ensure_type == VALIDATE_FD_FILE_TYPE_DIR || ensure_type == VALIDATE_FD_FILE_TYPE_ANY) && S_ISDIR (st_buf->st_mode)) + { + /* For dirs, we keep the dev/ino of the directory itself */ + dirname = g_strdup (path); + } + else + goto errout; + + dir_fd = open (dirname, O_CLOEXEC | O_PATH); + if (dir_fd < 0 || fstat (dir_fd, real_dir_st_buf) != 0) + goto errout; + + if (name != NULL) + { + if (fstatat (dir_fd, name, &real_st_buf, AT_SYMLINK_NOFOLLOW) < 0 || + st_buf->st_dev != real_st_buf.st_dev || + st_buf->st_ino != real_st_buf.st_ino) + goto errout; + } + else if (st_buf->st_dev != real_dir_st_buf->st_dev || + st_buf->st_ino != real_dir_st_buf->st_ino) + goto errout; + + + if (path_out) + *path_out = g_steal_pointer (&path); + + return TRUE; + + errout: + /* Don't leak any info about real file path existence, etc */ + g_set_error (error, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Invalid fd passed"); + return FALSE; +} + +static char * +verify_existing_document (struct stat *st_buf, + gboolean reuse_existing, + gboolean directory, + XdpAppInfo *app_info, + gboolean allow_write, + char **real_path_out) +{ + g_autoptr(PermissionDbEntry) old_entry = NULL; + g_autofree char *id = NULL; + + g_assert (st_buf->st_dev == fuse_dev); + + /* The passed in fd is on the fuse filesystem itself */ + id = xdp_fuse_lookup_id_for_inode (st_buf->st_ino, directory, real_path_out); + g_debug ("path on fuse, id %s", id); + if (id == NULL) + return NULL; + + /* Don't lock the db before doing the fuse call above, because it takes takes a lock + that can block something calling back, causing a deadlock on the db lock */ + XDP_AUTOLOCK (db); + + /* If the entry doesn't exist anymore, fail. Also fail if not + * reuse_existing, because otherwise the user could use this to + * get a copy with permissions and thus escape later permission + * revocations + */ + old_entry = permission_db_lookup (db, id); + if (old_entry == NULL || !reuse_existing) + return NULL; + + /* Don't allow re-exposing non-writable document as writable */ + if (allow_write && + !document_entry_has_permissions (old_entry, app_info, DOCUMENT_PERMISSION_FLAGS_WRITE)) + return NULL; + + return g_steal_pointer (&id); +} + +static void +portal_add (GDBusMethodInvocation *invocation, + GVariant *parameters, + XdpAppInfo *app_info) +{ + int fd_id; + gboolean reuse_existing, persistent; + DocumentAddFullFlags flags = 0; + GDBusMessage *message; + GUnixFDList *fd_list; + GError *error = NULL; + g_auto(GStrv) ids = NULL; + + g_variant_get (parameters, "(hbb)", &fd_id, &reuse_existing, &persistent); + + if (reuse_existing) + flags |= DOCUMENT_ADD_FLAGS_REUSE_EXISTING; + if (persistent) + flags |= DOCUMENT_ADD_FLAGS_PERSISTENT; + + message = g_dbus_method_invocation_get_message (invocation); + fd_list = g_dbus_message_get_unix_fd_list (message); + + if (fd_list != NULL) + { + int fds_len; + const int *fds = g_unix_fd_list_peek_fds (fd_list, &fds_len); + if (fd_id < fds_len) + { + int fd = fds[fd_id]; + + ids = document_add_full (&fd, NULL, NULL, 1, flags, app_info, "", 0, &error); + } + } + + if (ids == NULL) + { + g_dbus_method_invocation_take_error (invocation, error); + return; + } + + g_dbus_method_invocation_return_value (invocation, g_variant_new ("(s)", ids[0])); +} + +static char * +get_output (GError **error, + const char *argv0, + ...) +{ + gboolean res; + g_autofree char *output = NULL; + va_list ap; + + va_start (ap, argv0); + res = xdp_spawn (NULL, &output, 0, error, argv0, ap); + va_end (ap); + + if (res) + { + g_strchomp (output); + return g_steal_pointer (&output); + } + return NULL; +} + +/* out => + 0 == hidden + 1 == read-only + 2 == read-write +*/ +static void +metadata_check_file_access (const char *keyfile_path, + int *allow_host_out, + int *allow_home_out) +{ + g_autoptr(GKeyFile) keyfile = NULL; + g_auto(GStrv) fss = NULL; + + keyfile = g_key_file_new (); + if (!g_key_file_load_from_file (keyfile, keyfile_path, G_KEY_FILE_NONE, NULL)) + return; + + fss = g_key_file_get_string_list (keyfile, "Context", "filesystems", NULL, NULL); + if (fss) + { + int i; + for (i = 0; fss[i] != NULL; i++) + { + const char *fs = fss[i]; + + if (strcmp (fs, "!host") == 0) + *allow_host_out = 0; + if (strcmp (fs, "host:ro") == 0) + *allow_host_out = 1; + if (strcmp (fs, "host") == 0) + *allow_host_out = 2; + + if (strcmp (fs, "!home") == 0) + *allow_home_out = 0; + if (strcmp (fs, "home:ro") == 0) + *allow_home_out = 1; + if (strcmp (fs, "home") == 0) + *allow_home_out = 2; + } + } +} + +/* This is a simplified version that only looks at filesystem=host and + * filesystem=home, as such it should not cause false positives, but + * be may create a document for files that the app should have access + * to (e.g. when the app has a more strict access but the file is + * still accessible) */ +static gboolean +app_has_file_access_fallback (const char *target_app_id, + DocumentPermissionFlags target_perms, + const char *path) +{ + g_autofree char *user_metadata = NULL; + g_autofree char *system_metadata = NULL; + g_autofree char *user_override = NULL; + g_autofree char *system_override = NULL; + g_autofree char *user_global_override = NULL; + g_autofree char *system_global_override = NULL; + g_autofree char *homedir = NULL; + g_autofree char *canonical_path = NULL; + gboolean is_in_home = FALSE; + g_autofree char *user_installation = g_build_filename (g_get_user_data_dir (), "flatpak", NULL); + const char *system_installation = "/var/lib/flatpak"; + int allow_host = 0; + int allow_home = 0; + + if (g_str_has_prefix (path, "/usr") || g_str_has_prefix (path, "/app") || g_str_has_prefix (path, "/tmp")) + return FALSE; + + user_metadata = g_build_filename (user_installation, "app", target_app_id, "current/active/metadata", NULL); + system_metadata = g_build_filename (system_installation, "app", target_app_id, "current/active/metadata", NULL); + user_override = g_build_filename (user_installation, "overrides", target_app_id, NULL); + system_override = g_build_filename (system_installation, "overrides", target_app_id, NULL); + user_global_override = g_build_filename (user_installation, "overrides", "global", NULL); + system_global_override = g_build_filename (system_installation, "overrides", "global", NULL); + + metadata_check_file_access (system_metadata, &allow_host, &allow_home); + metadata_check_file_access (user_metadata, &allow_host, &allow_home); + metadata_check_file_access (system_global_override, &allow_host, &allow_home); + metadata_check_file_access (system_override, &allow_host, &allow_home); + metadata_check_file_access (user_global_override, &allow_host, &allow_home); + metadata_check_file_access (user_override, &allow_host, &allow_home); + + if (allow_host == 2 || + ((allow_host == 1) && + (target_perms & DOCUMENT_PERMISSION_FLAGS_WRITE) == 0)) + return TRUE; + + homedir = xdp_canonicalize_filename (g_get_home_dir ()); + canonical_path = xdp_canonicalize_filename (path); + + is_in_home = xdp_has_path_prefix (canonical_path, homedir); + + if (is_in_home && + ((allow_home == 2) || + (allow_home == 1 && (target_perms & DOCUMENT_PERMISSION_FLAGS_WRITE) == 0))) + return TRUE; + + return FALSE; +} + + +static gboolean +app_has_file_access (const char *target_app_id, + DocumentPermissionFlags target_perms, + const char *path) +{ + g_autoptr(GError) error = NULL; + g_autofree char *res = NULL; + g_autofree char *arg = NULL; + + if (target_app_id == NULL || target_app_id[0] == '\0') + return FALSE; + + if (g_str_has_prefix (target_app_id, "snap.")) + { + res = get_output (&error, "snap", "routine", "file-access", + target_app_id + strlen ("snap."), path, NULL); + } + else + { + /* First we try flatpak info --file-access=PATH APPID, which is supported on new versions */ + arg = g_strdup_printf ("--file-access=%s", path); + res = get_output (&error, "flatpak", "info", arg, target_app_id, NULL); + } + + if (res) + { + if (strcmp (res, "read-write") == 0) + return TRUE; + + if (strcmp (res, "read-only") == 0 && + ((target_perms & DOCUMENT_PERMISSION_FLAGS_WRITE) == 0)) + return TRUE; + + return FALSE; + } + + /* Secondly we fall back to a simple check that will not be perfect but should not + cause false positives. */ + return app_has_file_access_fallback (target_app_id, target_perms, path); +} + +static void +portal_add_full (GDBusMethodInvocation *invocation, + GVariant *parameters, + XdpAppInfo *app_info) +{ + g_autoptr(GVariant) array = NULL; + guint32 flags; + const char *target_app_id; + g_autofree const char **permissions = NULL; + DocumentPermissionFlags target_perms; + gsize n_args; + GDBusMessage *message; + GUnixFDList *fd_list; + g_autofree int *fd = NULL; + g_auto(GStrv) ids = NULL; + GError *error = NULL; + GVariantBuilder builder; + int fds_len; + int i; + const int *fds; + + g_variant_get (parameters, "(@ahu&s^a&s)", + &array, &flags, &target_app_id, &permissions); + + if ((flags & ~DOCUMENT_ADD_FLAGS_FLAGS_ALL) != 0) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Invalid flags"); + return; + } + + target_perms = xdp_parse_permissions (permissions, &error); + if (error) + { + g_dbus_method_invocation_take_error (invocation, error); + return; + } + + n_args = g_variant_n_children (array); + fd = g_new (int, n_args); + message = g_dbus_method_invocation_get_message (invocation); + fd_list = g_dbus_message_get_unix_fd_list (message); + + if (fd_list == NULL) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "No fds passed"); + return; + } + + fds_len = 0; + fds = g_unix_fd_list_peek_fds (fd_list, &fds_len); + for (i = 0; i < n_args; i++) + { + int fd_id; + g_variant_get_child (array, i, "h", &fd_id); + if (fd_id < fds_len) + fd[i] = fds[fd_id]; + else + fd[i] = -1; + } + + ids = document_add_full (fd, NULL, NULL, n_args, flags, app_info, target_app_id, target_perms, &error); + + if (ids == NULL) + { + g_dbus_method_invocation_take_error (invocation, error); + return; + } + + g_variant_builder_init (&builder, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add (&builder, "{sv}", "mountpoint", + g_variant_new_bytestring (xdp_fuse_get_mountpoint ())); + + g_dbus_method_invocation_return_value (invocation, + g_variant_new ("(^as@a{sv})", + (char **)ids, + g_variant_builder_end (&builder))); +} + +/* + * if the fd array contains fds that were not opened by the client itself, + * parent_dev and parent_ino must contain the st_dev/st_ino fields for the + * parent directory to check for, to prevent symlink attacks. + */ +char ** +document_add_full (int *fd, + int *parent_dev, + int *parent_ino, + int n_args, + DocumentAddFullFlags flags, + XdpAppInfo *app_info, + const char *target_app_id, + DocumentPermissionFlags target_perms, + GError **error) +{ + const char *app_id = xdp_app_info_get_id (app_info); + g_autoptr(GPtrArray) ids = g_ptr_array_new_with_free_func (g_free); + g_autoptr(GPtrArray) paths = g_ptr_array_new_with_free_func (g_free); + gboolean reuse_existing, persistent, as_needed_by_app, allow_write, is_dir; + g_autofree struct stat *real_dir_st_bufs = NULL; + struct stat st_buf; + g_autofree gboolean *writable = NULL; + int i; + + reuse_existing = (flags & DOCUMENT_ADD_FLAGS_REUSE_EXISTING) != 0; + persistent = (flags & DOCUMENT_ADD_FLAGS_PERSISTENT) != 0; + as_needed_by_app = (flags & DOCUMENT_ADD_FLAGS_AS_NEEDED_BY_APP) != 0; + is_dir = (flags & DOCUMENT_ADD_FLAGS_DIRECTORY) != 0; + allow_write = (target_perms & DOCUMENT_PERMISSION_FLAGS_WRITE) != 0; + + g_ptr_array_set_size (paths, n_args + 1); + g_ptr_array_set_size (ids, n_args + 1); + real_dir_st_bufs = g_new0 (struct stat, n_args); + writable = g_new0 (gboolean, n_args); + + for (i = 0; i < n_args; i++) + { + g_autofree char *path = NULL; + + if (!validate_fd (fd[i], app_info, is_dir ? VALIDATE_FD_FILE_TYPE_DIR : VALIDATE_FD_FILE_TYPE_REGULAR, &st_buf, &real_dir_st_bufs[i], &path, &writable[i], error)) + return NULL; + + if (parent_dev != NULL && parent_ino != NULL) + { + if (real_dir_st_bufs[i].st_dev != parent_dev[i] || + real_dir_st_bufs[i].st_ino != parent_ino[i]) + { + g_set_error (error, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Invalid parent directory"); + return NULL; + } + } + + if (allow_write && !writable[i]) + { + g_set_error (error, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Not enough permissions"); + return NULL; + } + + if (st_buf.st_dev == fuse_dev) + { + g_autofree char *real_path = NULL; + g_autofree char *id = NULL; + + /* The passed in fd is on the fuse filesystem itself */ + id = verify_existing_document (&st_buf, reuse_existing, is_dir, app_info, allow_write, &real_path); + if (id == NULL) + { + g_set_error (error, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Invalid fd passed"); + return NULL; + } + + /* Maybe this was a file on a directory document and we can expose the real path instead */ + if (real_path) + { + g_autofree char *dirname = NULL; + + g_free (path); + path = g_steal_pointer (&real_path); + /* Need to update real_dir_st_bufs */ + if (is_dir) + dirname = g_strdup (path); + else + dirname = g_path_get_dirname (path); + if (lstat (dirname, &real_dir_st_bufs[i]) != 0) + { + g_set_error (error, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Invalid fd passed"); + return NULL; + } + } + else + g_ptr_array_index(ids,i) = g_steal_pointer (&id); + } + + g_ptr_array_index(paths,i) = g_steal_pointer (&path); + } + + { + DocumentPermissionFlags caller_base_perms = DOCUMENT_PERMISSION_FLAGS_GRANT_PERMISSIONS | + DOCUMENT_PERMISSION_FLAGS_READ; + DocumentPermissionFlags caller_write_perms = DOCUMENT_PERMISSION_FLAGS_WRITE; + + /* If its a unique one its safe for the creator to delete it at will */ + if (!reuse_existing) + caller_write_perms |= DOCUMENT_PERMISSION_FLAGS_DELETE; + + XDP_AUTOLOCK (db); /* Lock once for all ops */ + + for (i = 0; i < n_args; i++) + { + const char *path = g_ptr_array_index(paths,i); + g_assert (path != NULL); + + if (as_needed_by_app && + app_has_file_access (target_app_id, target_perms, path)) + { + g_free (g_ptr_array_index(ids,i)); + g_ptr_array_index(ids,i) = g_strdup (""); + continue; + } + + if (g_ptr_array_index(ids,i) == NULL) + { + char *id = do_create_doc (&real_dir_st_bufs[i], path, reuse_existing, persistent, is_dir); + g_ptr_array_index(ids,i) = id; + + if (app_id[0] != '\0' && strcmp (app_id, target_app_id) != 0) + { + DocumentPermissionFlags caller_perms = caller_base_perms; + + if (writable[i]) + caller_perms |= caller_write_perms; + + g_autoptr(PermissionDbEntry) entry = permission_db_lookup (db, id);; + do_set_permissions (entry, id, app_id, caller_perms); + } + + if (target_app_id[0] != '\0' && target_perms != 0) + { + g_autoptr(PermissionDbEntry) entry = permission_db_lookup (db, id); + do_set_permissions (entry, id, target_app_id, target_perms); + } + } + } + } + + /* Invalidate with lock dropped to avoid deadlock */ + for (i = 0; i < n_args; i++) + { + const char *id = g_ptr_array_index (ids,i); + g_assert (id != NULL); + + if (*id == 0) + continue; + + xdp_fuse_invalidate_doc_app (id, NULL); + if (app_id[0] != '\0') + xdp_fuse_invalidate_doc_app (id, app_id); + if (target_app_id[0] != '\0' && target_perms != 0) + xdp_fuse_invalidate_doc_app (id, target_app_id); + } + + g_ptr_array_index(ids,n_args) = NULL; + + return g_strdupv ((char**)ids->pdata); +} + +static void +portal_add_named_full (GDBusMethodInvocation *invocation, + GVariant *parameters, + XdpAppInfo *app_info) +{ + const char *app_id = xdp_app_info_get_id (app_info); + GDBusMessage *message; + GUnixFDList *fd_list; + int parent_fd_id, parent_fd, fds_len; + g_autofree char *parent_path = NULL; + const int *fds = NULL; + struct stat parent_st_buf; + gboolean reuse_existing, persistent, as_needed_by_app; + guint32 flags = 0; + const char *filename; + const char *target_app_id; + g_autofree const char **permissions = NULL; + g_autofree char *id = NULL; + g_autofree char *path = NULL; + DocumentPermissionFlags target_perms; + GVariantBuilder builder; + g_autoptr(GVariant) filename_v = NULL; + GError *error = NULL; + + g_variant_get (parameters, "(h@ayus^a&s)", &parent_fd_id, &filename_v, &flags, &target_app_id, &permissions); + filename = g_variant_get_bytestring (filename_v); + + /* This is only allowed from the host, or else we could leak existence of files */ + if (!xdp_app_info_is_host (app_info)) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Not enough permissions"); + return; + } + + if ((flags & ~DOCUMENT_ADD_FLAGS_FLAGS_ALL) != 0 || + /* Don't support directory named documents */ + (flags & DOCUMENT_ADD_FLAGS_DIRECTORY) != 0) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Invalid flags"); + return; + } + + reuse_existing = (flags & DOCUMENT_ADD_FLAGS_REUSE_EXISTING) != 0; + persistent = (flags & DOCUMENT_ADD_FLAGS_PERSISTENT) != 0; + as_needed_by_app = (flags & DOCUMENT_ADD_FLAGS_AS_NEEDED_BY_APP) != 0; + + target_perms = xdp_parse_permissions (permissions, &error); + if (error) + { + g_dbus_method_invocation_take_error (invocation, error); + return; + } + + message = g_dbus_method_invocation_get_message (invocation); + fd_list = g_dbus_message_get_unix_fd_list (message); + + parent_fd = -1; + if (fd_list != NULL) + { + fds = g_unix_fd_list_peek_fds (fd_list, &fds_len); + if (parent_fd_id < fds_len) + parent_fd = fds[parent_fd_id]; + } + + if (strchr (filename, '/') != NULL || *filename == 0) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Invalid filename passed"); + return; + } + + parent_path = xdp_app_info_get_path_for_fd (app_info, parent_fd, S_IFDIR, &parent_st_buf, NULL, &error); + if (parent_path == NULL || parent_st_buf.st_dev == fuse_dev) + { + if (parent_path == NULL) + g_debug ("Invalid fd passed: %s", error->message); + else + g_debug ("Invalid fd passed: \"%s\" not on FUSE device", parent_path); + + /* Don't leak any info about real file path existence, etc */ + g_clear_error (&error); + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Invalid fd passed"); + return; + } + + path = g_build_filename (parent_path, filename, NULL); + + g_debug ("portal_add_named_full %s", path); + + { + DocumentPermissionFlags caller_perms = + DOCUMENT_PERMISSION_FLAGS_GRANT_PERMISSIONS | + DOCUMENT_PERMISSION_FLAGS_READ | + DOCUMENT_PERMISSION_FLAGS_WRITE; + + /* If its a unique one its safe for the creator to + delete it at will */ + if (!reuse_existing) + caller_perms |= DOCUMENT_PERMISSION_FLAGS_DELETE; + + XDP_AUTOLOCK (db); + + if (as_needed_by_app && + app_has_file_access (target_app_id, target_perms, path)) + { + id = g_strdup (""); + } + else + { + id = do_create_doc (&parent_st_buf, path, reuse_existing, persistent, FALSE); + + if (app_id[0] != '\0' && strcmp (app_id, target_app_id) != 0) + { + g_autoptr(PermissionDbEntry) entry = permission_db_lookup (db, id);; + do_set_permissions (entry, id, app_id, caller_perms); + } + + if (target_app_id[0] != '\0' && target_perms != 0) + { + g_autoptr(PermissionDbEntry) entry = permission_db_lookup (db, id); + do_set_permissions (entry, id, target_app_id, target_perms); + } + } + } + + /* Invalidate with lock dropped to avoid deadlock */ + g_assert (id != NULL); + + if (*id != 0) + { + xdp_fuse_invalidate_doc_app (id, NULL); + if (app_id[0] != '\0') + xdp_fuse_invalidate_doc_app (id, app_id); + if (target_app_id[0] != '\0' && target_perms != 0) + xdp_fuse_invalidate_doc_app (id, target_app_id); + } + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sv}")); + g_variant_builder_add (&builder, "{sv}", "mountpoint", + g_variant_new_bytestring (xdp_fuse_get_mountpoint ())); + + g_dbus_method_invocation_return_value (invocation, + g_variant_new ("(s@a{sv})", + id, + g_variant_builder_end (&builder))); +} + +static void +portal_add_named (GDBusMethodInvocation *invocation, + GVariant *parameters, + XdpAppInfo *app_info) +{ + GDBusMessage *message; + GUnixFDList *fd_list; + g_autofree char *id = NULL; + int parent_fd_id, parent_fd, fds_len; + const int *fds; + g_autofree char *parent_path = NULL; + g_autofree char *path = NULL; + struct stat parent_st_buf; + const char *filename; + gboolean reuse_existing, persistent; + g_autoptr(GError) local_error = NULL; + g_autoptr(GVariant) filename_v = NULL; + + g_variant_get (parameters, "(h@aybb)", &parent_fd_id, &filename_v, &reuse_existing, &persistent); + filename = g_variant_get_bytestring (filename_v); + + /* This is only allowed from the host, or else we could leak existence of files */ + if (!xdp_app_info_is_host (app_info)) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Not enough permissions"); + return; + } + + message = g_dbus_method_invocation_get_message (invocation); + fd_list = g_dbus_message_get_unix_fd_list (message); + + parent_fd = -1; + if (fd_list != NULL) + { + fds = g_unix_fd_list_peek_fds (fd_list, &fds_len); + if (parent_fd_id < fds_len) + parent_fd = fds[parent_fd_id]; + } + + if (strchr (filename, '/') != NULL || *filename == 0) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Invalid filename passed"); + return; + } + + parent_path = xdp_app_info_get_path_for_fd (app_info, parent_fd, S_IFDIR, &parent_st_buf, NULL, &local_error); + if (parent_path == NULL || parent_st_buf.st_dev == fuse_dev) + { + if (parent_path == NULL) + g_debug ("Invalid fd passed: %s", local_error->message); + else + g_debug ("Invalid fd passed: \"%s\" not on FUSE device", parent_path); + + /* Don't leak any info about real file path existence, etc */ + g_clear_error (&local_error); + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Invalid fd passed"); + return; + } + + path = g_build_filename (parent_path, filename, NULL); + + XDP_AUTOLOCK (db); + + id = do_create_doc (&parent_st_buf, path, reuse_existing, persistent, FALSE); + + g_dbus_method_invocation_return_value (invocation, + g_variant_new ("(s)", id)); +} + +typedef void (*PortalMethod) (GDBusMethodInvocation *invocation, + GVariant *parameters, + XdpAppInfo *app_info); + +static gboolean +handle_method (GCallback method_callback, + GDBusMethodInvocation *invocation) +{ + g_autoptr(GError) error = NULL; + g_autoptr(XdpAppInfo) app_info = NULL; + PortalMethod portal_method = (PortalMethod)method_callback; + + app_info = xdp_invocation_lookup_app_info_sync (invocation, NULL, &error); + if (app_info == NULL) + g_dbus_method_invocation_return_gerror (invocation, error); + else + portal_method (invocation, g_dbus_method_invocation_get_parameters (invocation), app_info); + + return TRUE; +} + +static gboolean +handle_get_mount_point (XdpDbusDocuments *object, GDBusMethodInvocation *invocation) +{ + if (fuse_dev == 0) + { + /* We mustn't reply to this until the FUSE mount point is open for + * business. */ + g_queue_push_tail (&get_mount_point_invocations, g_object_ref (invocation)); + return TRUE; + } + + xdp_dbus_documents_complete_get_mount_point (object, invocation, xdp_fuse_get_mountpoint ()); + return TRUE; +} + +static gboolean +portal_lookup (GDBusMethodInvocation *invocation, + GVariant *parameters, + XdpAppInfo *app_info) +{ + const char *filename; + g_autofree char *path = NULL; + xdp_autofd int fd = -1; + struct stat st_buf, real_dir_st_buf; + g_autofree char *id = NULL; + GError *error = NULL; + gboolean is_dir; + + if (!xdp_app_info_is_host (app_info)) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Not allowed in sandbox"); + return TRUE; + } + + g_variant_get (parameters, "(^&ay)", &filename); + + fd = open (filename, O_PATH | O_CLOEXEC); + if (fd == -1) + { + int errsv = errno; + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_FOUND, + "%s", g_strerror (errsv)); + return TRUE; + } + + if (!validate_fd (fd, app_info, VALIDATE_FD_FILE_TYPE_ANY, &st_buf, &real_dir_st_buf, &path, NULL, &error)) + { + g_dbus_method_invocation_take_error (invocation, error); + return TRUE; + } + + is_dir = S_ISDIR (st_buf.st_mode); + + if (st_buf.st_dev == fuse_dev) + { + /* The passed in fd is on the fuse filesystem itself */ + id = xdp_fuse_lookup_id_for_inode (st_buf.st_ino, is_dir, NULL); + g_debug ("path on fuse, id %s", id); + } + else + { + g_autoptr(GVariant) data = NULL; + g_autoptr(GVariant) data_transient = NULL; + g_auto(GStrv) ids = NULL; + guint32 flags = 0; + + if (is_dir) + flags |= DOCUMENT_ENTRY_FLAG_DIRECTORY; + + data = g_variant_ref_sink (g_variant_new ("(^ayttu)", + path, + (guint64)real_dir_st_buf.st_dev, + (guint64)real_dir_st_buf.st_ino, + flags)); + ids = permission_db_list_ids_by_value (db, data); + if (ids[0] != NULL) + id = g_strdup (ids[0]); + + if (id == NULL) + { + g_auto(GStrv) transient_ids = NULL; + data_transient = g_variant_ref_sink (g_variant_new ("(^ayttu)", + path, + (guint64)real_dir_st_buf.st_dev, + (guint64)real_dir_st_buf.st_ino, + flags|DOCUMENT_ENTRY_FLAG_TRANSIENT)); + transient_ids = permission_db_list_ids_by_value (db, data_transient); + if (transient_ids[0] != NULL) + id = g_strdup (transient_ids[0]); + } + } + + g_dbus_method_invocation_return_value (invocation, + g_variant_new ("(s)", id ? id : "")); + + return TRUE; +} + +static GVariant * +get_app_permissions (PermissionDbEntry *entry) +{ + g_autofree const char **apps = NULL; + GVariantBuilder builder; + int i; + + apps = permission_db_entry_list_apps (entry); + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sas}")); + + for (i = 0; apps[i] != NULL; i++) + { + g_autofree const char **permissions = permission_db_entry_list_permissions (entry, apps[i]); + g_variant_builder_add_value (&builder, + g_variant_new ("{s^as}", apps[i], permissions)); + } + + return g_variant_builder_end (&builder); +} + +static GVariant * +get_path (PermissionDbEntry *entry) +{ + g_autoptr (GVariant) data = permission_db_entry_get_data (entry); + const char *path; + + g_variant_get (data, "(^&ayttu)", &path, NULL, NULL, NULL); + return g_variant_new_bytestring (path); +} + +static gboolean +portal_info (GDBusMethodInvocation *invocation, + GVariant *parameters, + XdpAppInfo *app_info) +{ + const char *id = NULL; + g_autoptr(PermissionDbEntry) entry = NULL; + + if (!xdp_app_info_is_host (app_info)) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Not allowed in sandbox"); + return TRUE; + } + + g_variant_get (parameters, "(&s)", &id); + + XDP_AUTOLOCK (db); + + entry = permission_db_lookup (db, id); + + if (!entry) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Invalid ID passed"); + return TRUE; + } + + g_dbus_method_invocation_return_value (invocation, + g_variant_new ("(@ay@a{sas})", + get_path (entry), + get_app_permissions (entry))); + + return TRUE; +} + +static gboolean +portal_list (GDBusMethodInvocation *invocation, + GVariant *parameters, + XdpAppInfo *app_info) +{ + const char *app_id = xdp_app_info_get_id (app_info); + g_auto(GStrv) ids = NULL; + GVariantBuilder builder; + int i; + + if (!xdp_app_info_is_host (app_info)) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Not allowed in sandbox"); + return TRUE; + } + + g_variant_get (parameters, "(&s)", &app_id); + + XDP_AUTOLOCK (db); + + if (strcmp (app_id, "") == 0) + ids = permission_db_list_ids (db); + else + ids = permission_db_list_ids_by_app (db, app_id); + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{say}")); + for (i = 0; ids[i]; i++) + { + g_autoptr(PermissionDbEntry) entry = NULL; + + entry = permission_db_lookup (db, ids[i]); + + g_variant_builder_add (&builder, "{s@ay}", ids[i], get_path (entry)); + } + + g_dbus_method_invocation_return_value (invocation, + g_variant_new ("(@a{say})", + g_variant_builder_end (&builder))); + + return TRUE; +} + +static void +peer_died_cb (const char *name) +{ + stop_file_transfers_for_sender (name); +} + +static void +on_bus_acquired (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + GError *error = NULL; + GDBusInterfaceSkeleton *file_transfer; + + dbus_api = xdp_dbus_documents_skeleton_new (); + + xdp_dbus_documents_set_version (XDP_DBUS_DOCUMENTS (dbus_api), 4); + + g_signal_connect_swapped (dbus_api, "handle-get-mount-point", G_CALLBACK (handle_get_mount_point), NULL); + g_signal_connect_swapped (dbus_api, "handle-add", G_CALLBACK (handle_method), portal_add); + g_signal_connect_swapped (dbus_api, "handle-add-named", G_CALLBACK (handle_method), portal_add_named); + g_signal_connect_swapped (dbus_api, "handle-add-full", G_CALLBACK (handle_method), portal_add_full); + g_signal_connect_swapped (dbus_api, "handle-add-named-full", G_CALLBACK (handle_method), portal_add_named_full); + g_signal_connect_swapped (dbus_api, "handle-grant-permissions", G_CALLBACK (handle_method), portal_grant_permissions); + g_signal_connect_swapped (dbus_api, "handle-revoke-permissions", G_CALLBACK (handle_method), portal_revoke_permissions); + g_signal_connect_swapped (dbus_api, "handle-delete", G_CALLBACK (handle_method), portal_delete); + g_signal_connect_swapped (dbus_api, "handle-lookup", G_CALLBACK (handle_method), portal_lookup); + g_signal_connect_swapped (dbus_api, "handle-info", G_CALLBACK (handle_method), portal_info); + g_signal_connect_swapped (dbus_api, "handle-list", G_CALLBACK (handle_method), portal_list); + + file_transfer = file_transfer_create (); + g_dbus_interface_skeleton_set_flags (file_transfer, + G_DBUS_INTERFACE_SKELETON_FLAGS_HANDLE_METHOD_INVOCATIONS_IN_THREAD); + + xdp_connection_track_name_owners (connection, peer_died_cb); + + if (!g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (dbus_api), + connection, + "/org/freedesktop/portal/documents", + &error)) + { + g_warning ("error: %s", error->message); + g_error_free (error); + } + + g_debug ("Providing portal %s", g_dbus_interface_skeleton_get_info (G_DBUS_INTERFACE_SKELETON (dbus_api))->name); + + if (!g_dbus_interface_skeleton_export (file_transfer, + connection, + "/org/freedesktop/portal/documents", + &error)) + { + g_warning ("error: %s", error->message); + g_error_free (error); + } + + g_debug ("Providing portal %s", g_dbus_interface_skeleton_get_info (G_DBUS_INTERFACE_SKELETON (file_transfer))->name); +} + +static void +on_name_acquired (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + struct stat stbuf; + gpointer invocation; + + g_debug ("%s acquired", name); + + if (!xdp_fuse_init (&exit_error)) + { + final_exit_status = 6; + g_printerr ("fuse init failed: %s", exit_error->message); + g_main_loop_quit (loop); + return; + } + + if (stat (xdp_fuse_get_mountpoint (), &stbuf) != 0) + { + g_set_error (&exit_error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "fuse stat failed: %s", g_strerror (errno)); + final_exit_status = 7; + g_printerr ("fuse stat failed: %s", g_strerror (errno)); + g_main_loop_quit (loop); + return; + } + + fuse_dev = stbuf.st_dev; + + xdp_set_documents_mountpoint (xdp_fuse_get_mountpoint ()); + + while ((invocation = g_queue_pop_head (&get_mount_point_invocations)) != NULL) + { + xdp_dbus_documents_complete_get_mount_point (dbus_api, invocation, xdp_fuse_get_mountpoint ()); + g_object_unref (invocation); + } +} + +static void +on_name_lost (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + g_debug ("%s lost", name); + + if (final_exit_status == 0) + final_exit_status = 20; + + if (exit_error == NULL) + g_set_error (&exit_error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "D-Bus name \"%s\" lost", name); + + g_main_loop_quit (loop); +} + +gboolean +on_fuse_unmount (void *unused) +{ + if (!g_main_loop_is_running (loop)) + return G_SOURCE_REMOVE; + + g_debug ("fuse fs unmounted externally"); + + if (final_exit_status == 0) + final_exit_status = 21; + + if (exit_error == NULL) + g_set_error (&exit_error, G_IO_ERROR, G_IO_ERROR_FAILED, "Fuse filesystem unmounted"); + + g_main_loop_quit (loop); + + return G_SOURCE_REMOVE; +} + +static void +exit_handler (int sig) +{ + /* We cannot set exit_error here, because malloc() in a signal handler + * is undefined behaviour. Rely on main() coping gracefully with + * that. */ + g_main_loop_quit (loop); +} + +static void +session_bus_closed (GDBusConnection *connection, + gboolean remote_peer_vanished, + GError *bus_error) +{ + if (exit_error == NULL) + g_set_error (&exit_error, G_IO_ERROR, G_IO_ERROR_BROKEN_PIPE, "Disconnected from session bus"); + + g_main_loop_quit (loop); +} + +static int +set_one_signal_handler (int sig, + void (*handler)(int), + int remove) +{ + struct sigaction sa; + struct sigaction old_sa; + + memset (&sa, 0, sizeof (struct sigaction)); + sa.sa_handler = remove ? SIG_DFL : handler; + sigemptyset (&(sa.sa_mask)); + sa.sa_flags = 0; + + if (sigaction (sig, NULL, &old_sa) == -1) + { + g_warning ("cannot get old signal handler"); + return -1; + } + + if (old_sa.sa_handler == (remove ? handler : SIG_DFL) && + sigaction (sig, &sa, NULL) == -1) + { + g_warning ("cannot set signal handler"); + return -1; + } + + return 0; +} + +static gboolean opt_verbose; +static gboolean opt_replace; +static gboolean opt_version; + +static GOptionEntry entries[] = { + { "verbose", 'v', 0, G_OPTION_ARG_NONE, &opt_verbose, "Print debug information", NULL }, + { "replace", 'r', 0, G_OPTION_ARG_NONE, &opt_replace, "Replace", NULL }, + { "version", 0, 0, G_OPTION_ARG_NONE, &opt_version, "Print version and exit", NULL }, + { NULL } +}; + +static void +message_handler (const gchar *log_domain, + GLogLevelFlags log_level, + const gchar *message, + gpointer user_data) +{ + /* Make this look like normal console output */ + if (log_level & G_LOG_LEVEL_DEBUG) + printf ("XDP: %s\n", message); + else + printf ("%s: %s\n", g_get_prgname (), message); +} + +static void +printerr_handler (const gchar *string) +{ + fprintf (stderr, "error: %s\n", string); +} + +int +main (int argc, + char **argv) +{ + guint owner_id; + + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + GDBusConnection *session_bus; + g_autoptr(GOptionContext) context = NULL; + GDBusMethodInvocation *invocation; + + g_log_writer_default_set_use_stderr (TRUE); + + setlocale (LC_ALL, ""); + + /* Avoid even loading gvfs to avoid accidental confusion */ + g_setenv ("GIO_USE_VFS", "local", TRUE); + + g_set_printerr_handler (printerr_handler); + + context = g_option_context_new ("- document portal"); + g_option_context_add_main_entries (context, entries, NULL); + if (!g_option_context_parse (context, &argc, &argv, &error)) + { + g_printerr ("Option parsing failed: %s", error->message); + return 1; + } + + if (opt_version) + { + g_print ("%s\n", PACKAGE_STRING); + exit (EXIT_SUCCESS); + } + + if (opt_verbose) + g_log_set_handler (G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, message_handler, NULL); + + g_set_prgname (argv[0]); + + loop = g_main_loop_new (NULL, FALSE); + + path = g_build_filename (g_get_user_data_dir (), "flatpak/db", TABLE_NAME, NULL); + db = permission_db_new (path, FALSE, &error); + if (db == NULL) + { + g_printerr ("Failed to load db from '%s': %s", path, error->message); + exit (2); + } + + session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); + if (session_bus == NULL) + { + g_printerr ("No session bus: %s", error->message); + exit (3); + } + + permission_store = xdg_permission_store_proxy_new_sync (session_bus, G_DBUS_PROXY_FLAGS_NONE, + "org.freedesktop.impl.portal.PermissionStore", + "/org/freedesktop/impl/portal/PermissionStore", + NULL, &error); + if (permission_store == NULL) + { + g_print ("No permission store: %s", error->message); + exit (4); + } + + /* We want do do our custom post-mainloop exit */ + g_dbus_connection_set_exit_on_close (session_bus, FALSE); + + g_signal_connect (session_bus, "closed", G_CALLBACK (session_bus_closed), NULL); + + if (set_one_signal_handler (SIGHUP, exit_handler, 0) == -1 || + set_one_signal_handler (SIGINT, exit_handler, 0) == -1 || + set_one_signal_handler (SIGTERM, exit_handler, 0) == -1 || + set_one_signal_handler (SIGPIPE, SIG_IGN, 0) == -1) + exit (5); + + owner_id = g_bus_own_name (G_BUS_TYPE_SESSION, + "org.freedesktop.portal.Documents", + G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | (opt_replace ? G_BUS_NAME_OWNER_FLAGS_REPLACE : 0), + on_bus_acquired, + on_name_acquired, + on_name_lost, + NULL, + NULL); + + g_main_loop_run (loop); + + while ((invocation = g_queue_pop_head (&get_mount_point_invocations)) != NULL) + { + if (exit_error != NULL) + g_dbus_method_invocation_return_gerror (invocation, exit_error); + else + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "Terminated"); + + g_object_unref (invocation); + } + + xdp_fuse_exit (); + + g_bus_unown_name (owner_id); + + return final_exit_status; +} diff --git a/document-portal/document-portal.h b/document-portal/document-portal.h new file mode 100644 index 0000000..e9ddc4c --- /dev/null +++ b/document-portal/document-portal.h @@ -0,0 +1,49 @@ +/* + * Copyright © 2018 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +#pragma once + +#include +#include "document-enums.h" + +typedef enum { + VALIDATE_FD_FILE_TYPE_REGULAR, + VALIDATE_FD_FILE_TYPE_DIR, + VALIDATE_FD_FILE_TYPE_ANY, +} ValidateFdType; + +gboolean validate_fd (int fd, + XdpAppInfo *app_info, + ValidateFdType ensure_type, + struct stat *st_buf, + struct stat *real_parent_st_buf, + char **path_out, + gboolean *writable_out, + GError **error); + +char ** document_add_full (int *fd, + int *parent_dev, + int *parent_ino, + int n_args, + DocumentAddFullFlags flags, + XdpAppInfo *app_info, + const char *target_app_id, + DocumentPermissionFlags target_perms, + GError **error); diff --git a/document-portal/document-store.c b/document-portal/document-store.c new file mode 100644 index 0000000..9efb8d6 --- /dev/null +++ b/document-portal/document-store.c @@ -0,0 +1,158 @@ +#include "config.h" +#include +#include +#include +#include "document-store.h" + +const char ** +xdg_unparse_permissions (DocumentPermissionFlags permissions) +{ + GPtrArray *array; + + array = g_ptr_array_new (); + + if (permissions & DOCUMENT_PERMISSION_FLAGS_READ) + g_ptr_array_add (array, "read"); + if (permissions & DOCUMENT_PERMISSION_FLAGS_WRITE) + g_ptr_array_add (array, "write"); + if (permissions & DOCUMENT_PERMISSION_FLAGS_GRANT_PERMISSIONS) + g_ptr_array_add (array, "grant-permissions"); + if (permissions & DOCUMENT_PERMISSION_FLAGS_DELETE) + g_ptr_array_add (array, "delete"); + + g_ptr_array_add (array, NULL); + return (const char **) g_ptr_array_free (array, FALSE); +} + +DocumentPermissionFlags +xdp_parse_permissions (const char **permissions, + GError **error) +{ + DocumentPermissionFlags perms; + int i; + + perms = 0; + for (i = 0; permissions[i]; i++) + { + if (strcmp (permissions[i], "read") == 0) + perms |= DOCUMENT_PERMISSION_FLAGS_READ; + else if (strcmp (permissions[i], "write") == 0) + perms |= DOCUMENT_PERMISSION_FLAGS_WRITE; + else if (strcmp (permissions[i], "grant-permissions") == 0) + perms |= DOCUMENT_PERMISSION_FLAGS_GRANT_PERMISSIONS; + else if (strcmp (permissions[i], "delete") == 0) + perms |= DOCUMENT_PERMISSION_FLAGS_DELETE; + else + { + g_set_error (error, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "No such permission"); + return 0; + } + } + + return perms; +} + +DocumentPermissionFlags +document_entry_get_permissions_by_app_id (PermissionDbEntry *entry, + const char *app_id) +{ + g_autofree const char **permissions = NULL; + + if (strcmp (app_id, "") == 0) + return DOCUMENT_PERMISSION_FLAGS_ALL; + + permissions = permission_db_entry_list_permissions (entry, app_id); + return xdp_parse_permissions (permissions, NULL); +} + +DocumentPermissionFlags +document_entry_get_permissions (PermissionDbEntry *entry, + XdpAppInfo *app_info) +{ + const char *app_id = xdp_app_info_get_id (app_info); + + if (xdp_app_info_is_host (app_info)) + return DOCUMENT_PERMISSION_FLAGS_ALL; + + return document_entry_get_permissions_by_app_id (entry, app_id); +} + +gboolean +document_entry_has_permissions_by_app_id (PermissionDbEntry *entry, + const char *app_id, + DocumentPermissionFlags perms) +{ + DocumentPermissionFlags current_perms; + + current_perms = document_entry_get_permissions_by_app_id (entry, app_id); + + return (current_perms & perms) == perms; +} + +gboolean +document_entry_has_permissions (PermissionDbEntry *entry, + XdpAppInfo *app_info, + DocumentPermissionFlags perms) +{ + DocumentPermissionFlags current_perms; + + current_perms = document_entry_get_permissions (entry, app_info); + + return (current_perms & perms) == perms; +} + +char * +xdp_name_from_id (guint32 doc_id) +{ + return g_strdup_printf ("%x", doc_id); +} + +const char * +document_entry_get_path (PermissionDbEntry *entry) +{ + g_autoptr(GVariant) v = permission_db_entry_get_data (entry); + g_autoptr(GVariant) c = g_variant_get_child_value (v, 0); + return g_variant_get_bytestring (c); +} + +char * +document_entry_dup_basename (PermissionDbEntry *entry) +{ + const char *path = document_entry_get_path (entry); + + return g_path_get_basename (path); +} + +char * +document_entry_dup_dirname (PermissionDbEntry *entry) +{ + const char *path = document_entry_get_path (entry); + + return g_path_get_dirname (path); +} + +guint64 +document_entry_get_device (PermissionDbEntry *entry) +{ + g_autoptr(GVariant) v = permission_db_entry_get_data (entry); + g_autoptr(GVariant) c = g_variant_get_child_value (v, 1); + return g_variant_get_uint64 (c); +} + +guint64 +document_entry_get_inode (PermissionDbEntry *entry) +{ + g_autoptr(GVariant) v = permission_db_entry_get_data (entry); + g_autoptr(GVariant) c = g_variant_get_child_value (v, 2); + return g_variant_get_uint64 (c); +} + +guint32 +document_entry_get_flags (PermissionDbEntry *entry) +{ + g_autoptr(GVariant) v = permission_db_entry_get_data (entry); + g_autoptr(GVariant) c = g_variant_get_child_value (v, 3); + return g_variant_get_uint32 (c); +} diff --git a/document-portal/document-store.h b/document-portal/document-store.h new file mode 100644 index 0000000..91d1411 --- /dev/null +++ b/document-portal/document-store.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include "permission-db.h" +#include "document-enums.h" +#include "src/xdp-utils.h" + +G_BEGIN_DECLS + +#define DOCUMENT_ENTRY_FLAG_UNIQUE (1 << 0) +#define DOCUMENT_ENTRY_FLAG_TRANSIENT (1 << 1) +#define DOCUMENT_ENTRY_FLAG_DIRECTORY (1 << 2) + +const char ** xdg_unparse_permissions (DocumentPermissionFlags permissions); +DocumentPermissionFlags xdp_parse_permissions (const char **permissions, + GError **error); + +DocumentPermissionFlags document_entry_get_permissions_by_app_id (PermissionDbEntry *entry, + const char *app_id); +DocumentPermissionFlags document_entry_get_permissions (PermissionDbEntry *entry, + XdpAppInfo *app_info); +gboolean document_entry_has_permissions (PermissionDbEntry *entry, + XdpAppInfo *app_info, + DocumentPermissionFlags perms); +gboolean document_entry_has_permissions_by_app_id (PermissionDbEntry *entry, + const char *app_id, + DocumentPermissionFlags perms); +const char * document_entry_get_path (PermissionDbEntry *entry); +char * document_entry_dup_basename (PermissionDbEntry *entry); +char * document_entry_dup_dirname (PermissionDbEntry *entry); +guint64 document_entry_get_device (PermissionDbEntry *entry); +guint64 document_entry_get_inode (PermissionDbEntry *entry); +guint32 document_entry_get_flags (PermissionDbEntry *entry); + +char * xdp_name_from_id (guint32 doc_id); + + +G_END_DECLS diff --git a/document-portal/file-transfer.c b/document-portal/file-transfer.c new file mode 100644 index 0000000..028a210 --- /dev/null +++ b/document-portal/file-transfer.c @@ -0,0 +1,580 @@ +/* + * Copyright © 2018 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +#include "config.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include "file-transfer.h" +#include "src/xdp-utils.h" +#include "document-portal-dbus.h" +#include "document-enums.h" +#include "document-portal.h" +#include "document-portal-fuse.h" + +static XdpDbusFileTransfer *file_transfer; + +typedef struct +{ + char *path; + int parent_dev; + int parent_ino; +} ExportedFile; + +static void +exported_file_free (gpointer data) +{ + ExportedFile *file = data; + + g_free (file->path); + g_free (file); +} + +typedef struct +{ + GObject object; + GMutex mutex; + + GPtrArray *files; + gboolean writable; + gboolean autostop; + char *key; + char *sender; + XdpAppInfo *app_info; +} FileTransfer; + +typedef struct +{ + GObjectClass parent_class; +} FileTransferClass; + +static GType file_transfer_get_type (void); + +G_DEFINE_TYPE (FileTransfer, file_transfer, G_TYPE_OBJECT) + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (FileTransfer, g_object_unref); + +static void +file_transfer_init (FileTransfer *transfer) +{ + g_mutex_init (&transfer->mutex); +} + +static void +file_transfer_finalize (GObject *object) +{ + FileTransfer *transfer = (FileTransfer *)object; + + g_mutex_clear (&transfer->mutex); + xdp_app_info_unref (transfer->app_info); + g_ptr_array_unref (transfer->files); + g_free (transfer->key); + g_free (transfer->sender); + + G_OBJECT_CLASS (file_transfer_parent_class)->finalize (object); +} + +static void +file_transfer_class_init (FileTransferClass *class) +{ + G_OBJECT_CLASS (class)->finalize = file_transfer_finalize; +} + +static inline void +auto_unlock_unref_helper (FileTransfer **transfer) +{ + if (!*transfer) + return; + + g_mutex_unlock (&(*transfer)->mutex); + g_object_unref (*transfer); +} + +static inline FileTransfer * +auto_lock_helper (FileTransfer *transfer) +{ + if (transfer) + g_mutex_lock (&transfer->mutex); + return transfer; +} + +#define TRANSFER_AUTOLOCK_UNREF(transfer) \ + G_GNUC_UNUSED __attribute__((cleanup (auto_unlock_unref_helper))) \ + FileTransfer * G_PASTE (auto_unlock_unref, __LINE__) = \ + auto_lock_helper (transfer); + +G_LOCK_DEFINE (transfers); +static GHashTable *transfers; + +static FileTransfer * +lookup_transfer (const char *key) +{ + FileTransfer *transfer; + + G_LOCK (transfers); + transfer = (FileTransfer *)g_hash_table_lookup (transfers, key); + if (transfer) + g_object_ref (transfer); + G_UNLOCK (transfers); + + return transfer; +} + +static FileTransfer * +file_transfer_start (XdpAppInfo *app_info, + const char *sender, + gboolean writable, + gboolean autostop) +{ + FileTransfer *transfer; + + transfer = g_object_new (file_transfer_get_type (), NULL); + + transfer->app_info = xdp_app_info_ref (app_info); + transfer->sender = g_strdup (sender); + transfer->writable = writable; + transfer->autostop = autostop; + transfer->files = g_ptr_array_new_with_free_func (exported_file_free); + + G_LOCK (transfers); + do { + guint64 key; + g_free (transfer->key); + key = g_random_int (); + key = (key << 32) | g_random_int (); + transfer->key = g_strdup_printf ("%lu", key); + } + while (g_hash_table_contains (transfers, transfer->key)); + g_hash_table_insert (transfers, transfer->key, g_object_ref (transfer)); + G_UNLOCK (transfers); + + g_debug ("start file transfer owned by '%s' (%s)", + xdp_app_info_get_id (transfer->app_info), + transfer->sender); + + return transfer; +} + +static gboolean +stop (gpointer data) +{ + FileTransfer *transfer = data; + + g_object_unref (transfer); + + return G_SOURCE_REMOVE; +} + +static void +file_transfer_stop (FileTransfer *transfer) +{ + GDBusConnection *bus; + + g_debug ("stop file transfer owned by '%s' (%s)", + xdp_app_info_get_id (transfer->app_info), + transfer->sender); + + bus = g_dbus_interface_skeleton_get_connection (G_DBUS_INTERFACE_SKELETON (file_transfer)); + g_dbus_connection_emit_signal (bus, + transfer->sender, + "/org/freedesktop/portal/documents", + "org.freedesktop.portal.FileTransfer", + "TransferClosed", + g_variant_new ("(s)", transfer->key), + NULL); + + G_LOCK (transfers); + g_hash_table_steal (transfers, transfer->key); + G_UNLOCK (transfers); + + g_idle_add (stop, transfer); +} + +static void +file_transfer_add_file (FileTransfer *transfer, + const char *path, + struct stat *parent_st_buf) +{ + ExportedFile *file; + + file = g_new (ExportedFile, 1); + file->path = g_strdup (path); + file->parent_dev = parent_st_buf->st_dev; + file->parent_ino = parent_st_buf->st_ino; + + g_ptr_array_add (transfer->files, file); +} + +static char ** +file_transfer_execute (FileTransfer *transfer, + XdpAppInfo *target_app_info, + GError **error) +{ + guint32 flags; + DocumentPermissionFlags perms; + const char *target_app_id; + int n_fds; + g_autofree int *fds = NULL; + g_autofree int *parent_devs = NULL; + g_autofree int *parent_inos = NULL; + int i; + g_auto(GStrv) ids = NULL; + char **files = NULL; + + g_debug ("retrieve %d files for %s from file transfer owned by '%s' (%s)", + transfer->files->len, + xdp_app_info_get_id (target_app_info), + xdp_app_info_get_id (transfer->app_info), + transfer->sender); + + /* if the target is unsandboxed, just return the files as-is */ + if (xdp_app_info_is_host (target_app_info)) + { + files = g_new (char *, transfer->files->len + 1); + for (i = 0; i < transfer->files->len; i++) + { + ExportedFile *file = (ExportedFile*)g_ptr_array_index (transfer->files, i); + files[i] = g_strdup (file->path); + } + files[i] = NULL; + return files; + } + + flags = DOCUMENT_ADD_FLAGS_REUSE_EXISTING | DOCUMENT_ADD_FLAGS_AS_NEEDED_BY_APP; + + perms = DOCUMENT_PERMISSION_FLAGS_READ; + if (transfer->writable) + perms |= DOCUMENT_PERMISSION_FLAGS_WRITE; + + target_app_id = xdp_app_info_get_id (target_app_info); + + n_fds = transfer->files->len; + fds = g_new (int, n_fds); + parent_devs = g_new (int, n_fds); + parent_inos = g_new (int, n_fds); + for (i = 0; i < n_fds; i++) + { + ExportedFile *file = (ExportedFile*)g_ptr_array_index (transfer->files, i); + + fds[i] = open (file->path, O_PATH | O_CLOEXEC); + if (fds[i] == -1) + { + g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errno), "File transfer %s failed", transfer->key); + for (; i > 0; i--) + close (fds[i - 1]); + return NULL; + } + + parent_devs[i] = file->parent_dev; + parent_inos[i] = file->parent_ino; + } + + ids = document_add_full (fds, parent_devs, parent_inos, n_fds, flags, transfer->app_info, target_app_id, perms, error); + + for (i = 0; i < n_fds; i++) + close (fds[i]); + + if (ids) + { + const char *mountpoint = xdp_fuse_get_mountpoint (); + files = g_new (char *, n_fds + 1); + for (i = 0; i < n_fds; i++) + { + ExportedFile *file = (ExportedFile *) g_ptr_array_index (transfer->files, i); + + if (ids[i][0] == '\0') + files[i] = g_strdup (file->path); + else + { + g_autofree char *name = g_path_get_basename (file->path); + files[i] = g_build_filename (mountpoint, ids[i], name, NULL); + } + } + files[n_fds] = NULL; + } + + return files; +} + +static void +start_transfer (GDBusMethodInvocation *invocation, + GVariant *parameters, + XdpAppInfo *app_info) +{ + g_autoptr(GVariant) options = NULL; + g_autoptr(FileTransfer) transfer = NULL; + gboolean writable; + gboolean autostop; + const char *sender; + + g_variant_get (parameters, "(@a{sv})", &options); + if (!g_variant_lookup (options, "writable", "b", &writable)) + writable = FALSE; + + if (!g_variant_lookup (options, "autostop", "b", &autostop)) + autostop = TRUE; + + sender = g_dbus_method_invocation_get_sender (invocation); + + transfer = file_transfer_start (app_info, sender, writable, autostop); + + g_dbus_method_invocation_return_value (invocation, g_variant_new ("(s)", transfer->key)); +} + +static void +add_files (GDBusMethodInvocation *invocation, + GVariant *parameters, + XdpAppInfo *app_info) +{ + FileTransfer *transfer; + const char *key; + g_autoptr(GVariant) options = NULL; + GDBusMessage *message; + GUnixFDList *fd_list; + g_autoptr(GVariantIter) iter = NULL; + int fd_id; + const int *fds; + int n_fds; + + g_variant_get (parameters, "(&sah@a{sv})", &key, &iter, &options); + + transfer = lookup_transfer (key); + if (transfer == NULL) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid transfer"); + return; + } + + TRANSFER_AUTOLOCK_UNREF (transfer); + + if (strcmp (transfer->sender, g_dbus_method_invocation_get_sender (invocation)) != 0) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid transfer"); + return; + } + + message = g_dbus_method_invocation_get_message (invocation); + fd_list = g_dbus_message_get_unix_fd_list (message); + + if (fd_list == NULL) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_INVALID_ARGS, + "Invalid transfer"); + return; + } + + fds = g_unix_fd_list_peek_fds (fd_list, &n_fds); + + g_debug ("add %d files to file transfer owned by '%s' (%s)", n_fds, + xdp_app_info_get_id (transfer->app_info), + transfer->sender); + + while (g_variant_iter_next (iter, "h", &fd_id)) + { + int fd = -1; + g_autofree char *path = NULL; + gboolean fd_is_writable; + struct stat st_buf; + struct stat parent_st_buf; + + if (fd_id < n_fds) + fd = fds[fd_id]; + + if (fd == -1) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid transfer"); + return; + } + + if (!validate_fd (fd, app_info, VALIDATE_FD_FILE_TYPE_REGULAR, &st_buf, &parent_st_buf, &path, &fd_is_writable, NULL) || + (transfer->writable && !fd_is_writable)) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Can't export file"); + return; + } + + file_transfer_add_file (transfer, path, &parent_st_buf); + } + + g_dbus_method_invocation_return_value (invocation, NULL); +} + + +static void +retrieve_files (GDBusMethodInvocation *invocation, + GVariant *parameters, + XdpAppInfo *app_info) +{ + const char *key; + FileTransfer *transfer; + g_auto(GStrv) files = NULL; + GError *error = NULL; + + g_variant_get (parameters, "(&s@a{sv})", &key, NULL); + + transfer = lookup_transfer (key); + if (transfer == NULL) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid transfer"); + return; + } + + TRANSFER_AUTOLOCK_UNREF (transfer); + + files = file_transfer_execute (transfer, app_info, &error); + if (files == NULL) + g_dbus_method_invocation_return_gerror (invocation, error); + else + g_dbus_method_invocation_return_value (invocation, g_variant_new ("(^as)", files)); + + if (transfer->autostop) + file_transfer_stop (transfer); +} + +static void +stop_transfer (GDBusMethodInvocation *invocation, + GVariant *parameters, + XdpAppInfo *app_info) +{ + const char *key; + FileTransfer *transfer; + + g_variant_get (parameters, "(&s)", &key); + + transfer = lookup_transfer (key); + if (transfer == NULL) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid transfer"); + return; + } + + TRANSFER_AUTOLOCK_UNREF (transfer); + + file_transfer_stop (transfer); + + g_dbus_method_invocation_return_value (invocation, NULL); +} + +typedef void (*PortalMethod) (GDBusMethodInvocation *invocation, + GVariant *parameters, + XdpAppInfo *app_info); + +static gboolean +handle_method (GCallback method_callback, + GDBusMethodInvocation *invocation) +{ + g_autoptr(GError) error = NULL; + g_autoptr(XdpAppInfo) app_info = NULL; + PortalMethod portal_method = (PortalMethod)method_callback; + + app_info = xdp_invocation_lookup_app_info_sync (invocation, NULL, &error); + if (app_info == NULL) + g_dbus_method_invocation_return_gerror (invocation, error); + else + portal_method (invocation, g_dbus_method_invocation_get_parameters (invocation), app_info); + + return TRUE; +} + +GDBusInterfaceSkeleton * +file_transfer_create (void) +{ + file_transfer = xdp_dbus_file_transfer_skeleton_new (); + + g_signal_connect_swapped (file_transfer, "handle-start-transfer", G_CALLBACK (handle_method), start_transfer); + g_signal_connect_swapped (file_transfer, "handle-add-files", G_CALLBACK (handle_method), add_files); + g_signal_connect_swapped (file_transfer, "handle-retrieve-files", G_CALLBACK (handle_method), retrieve_files); + g_signal_connect_swapped (file_transfer, "handle-stop-transfer", G_CALLBACK (handle_method), stop_transfer); + + xdp_dbus_file_transfer_set_version (XDP_DBUS_FILE_TRANSFER (file_transfer), 1); + + transfers = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, g_object_unref); + + return G_DBUS_INTERFACE_SKELETON (file_transfer); +} + +void +stop_file_transfers_in_thread_func (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + const char *sender = (const char *)task_data; + GHashTableIter iter; + FileTransfer *transfer; + + G_LOCK (transfers); + if (transfers) + { + g_hash_table_iter_init (&iter, transfers); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&transfer)) + { + if (strcmp (sender, transfer->sender) == 0) + { + g_print ("removing transfer %s for dead peer %s\n", transfer->key, transfer->sender); + g_hash_table_iter_remove (&iter); + } + } + } + G_UNLOCK (transfers); +} + +void +stop_file_transfers_for_sender (const char *sender) +{ + GTask *task; + + task = g_task_new (NULL, NULL, NULL, NULL); + g_task_set_task_data (task, g_strdup (sender), g_free); + g_task_run_in_thread (task, stop_file_transfers_in_thread_func); + g_object_unref (task); +} diff --git a/document-portal/file-transfer.h b/document-portal/file-transfer.h new file mode 100644 index 0000000..7e0a415 --- /dev/null +++ b/document-portal/file-transfer.h @@ -0,0 +1,27 @@ +/* + * Copyright © 2018 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +#pragma once + +#include + +GDBusInterfaceSkeleton *file_transfer_create (void); + +void stop_file_transfers_for_sender (const char *name); diff --git a/document-portal/gvdb/README b/document-portal/gvdb/README new file mode 100644 index 0000000..94e6c5d --- /dev/null +++ b/document-portal/gvdb/README @@ -0,0 +1,7 @@ +DO NOT MODIFY ANY FILE IN THIS DIRECTORY + +(except maybe the Makefile.am) + +This directory is the result of a git subtree merge with the 'gvdb' +module on git.gnome.org. Please apply fixes to the 'gvdb' module and +perform a git merge. diff --git a/document-portal/gvdb/gvdb-builder.c b/document-portal/gvdb/gvdb-builder.c new file mode 100644 index 0000000..d40d341 --- /dev/null +++ b/document-portal/gvdb/gvdb-builder.c @@ -0,0 +1,539 @@ +/* + * Copyright © 2010 Codethink Limited + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the licence, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + * + * Author: Ryan Lortie + */ + +#include "gvdb-builder.h" +#include "gvdb-format.h" + +#include +#include +#if !defined(G_OS_WIN32) || !defined(_MSC_VER) +#include +#endif +#include + + +struct _GvdbItem +{ + gchar *key; + guint32 hash_value; + guint32_le assigned_index; + GvdbItem *parent; + GvdbItem *sibling; + GvdbItem *next; + + /* one of: + * this: + */ + GVariant *value; + + /* this: */ + GHashTable *table; + + /* or this: */ + GvdbItem *child; +}; + +static void +gvdb_item_free (gpointer data) +{ + GvdbItem *item = data; + + g_free (item->key); + + if (item->value) + g_variant_unref (item->value); + + if (item->table) + g_hash_table_unref (item->table); + + g_slice_free (GvdbItem, item); +} + +GHashTable * +gvdb_hash_table_new (GHashTable *parent, + const gchar *name_in_parent) +{ + GHashTable *table; + + table = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, gvdb_item_free); + + if (parent) + { + GvdbItem *item; + + item = gvdb_hash_table_insert (parent, name_in_parent); + gvdb_item_set_hash_table (item, table); + } + + return table; +} + +static guint32 +djb_hash (const gchar *key) +{ + guint32 hash_value = 5381; + + while (*key) + hash_value = hash_value * 33 + *(signed char *)key++; + + return hash_value; +} + +GvdbItem * +gvdb_hash_table_insert (GHashTable *table, + const gchar *key) +{ + GvdbItem *item; + + item = g_slice_new0 (GvdbItem); + item->key = g_strdup (key); + item->hash_value = djb_hash (key); + + g_hash_table_insert (table, g_strdup (key), item); + + return item; +} + +void +gvdb_hash_table_insert_string (GHashTable *table, + const gchar *key, + const gchar *value) +{ + GvdbItem *item; + + item = gvdb_hash_table_insert (table, key); + gvdb_item_set_value (item, g_variant_new_string (value)); +} + +void +gvdb_item_set_value (GvdbItem *item, + GVariant *value) +{ + g_return_if_fail (!item->value && !item->table && !item->child); + + item->value = g_variant_ref_sink (value); +} + +void +gvdb_item_set_hash_table (GvdbItem *item, + GHashTable *table) +{ + g_return_if_fail (!item->value && !item->table && !item->child); + + item->table = g_hash_table_ref (table); +} + +void +gvdb_item_set_parent (GvdbItem *item, + GvdbItem *parent) +{ + GvdbItem **node; + + g_return_if_fail (g_str_has_prefix (item->key, parent->key)); + g_return_if_fail (!parent->value && !parent->table); + g_return_if_fail (!item->parent && !item->sibling); + + for (node = &parent->child; *node; node = &(*node)->sibling) + if (strcmp ((*node)->key, item->key) > 0) + break; + + item->parent = parent; + item->sibling = *node; + *node = item; +} + +typedef struct +{ + GvdbItem **buckets; + gint n_buckets; +} HashTable; + +static HashTable * +hash_table_new (gint n_buckets) +{ + HashTable *table; + + table = g_slice_new (HashTable); + table->buckets = g_new0 (GvdbItem *, n_buckets); + table->n_buckets = n_buckets; + + return table; +} + +static void +hash_table_free (HashTable *table) +{ + g_free (table->buckets); + + g_slice_free (HashTable, table); +} + +static void +hash_table_insert (gpointer key, + gpointer value, + gpointer data) +{ + guint32 hash_value, bucket; + HashTable *table = data; + GvdbItem *item = value; + + hash_value = djb_hash (key); + bucket = hash_value % table->n_buckets; + item->next = table->buckets[bucket]; + table->buckets[bucket] = item; +} + +static guint32_le +item_to_index (GvdbItem *item) +{ + if (item != NULL) + return item->assigned_index; + + return guint32_to_le (-1u); +} + +typedef struct +{ + GQueue *chunks; + guint64 offset; + gboolean byteswap; +} FileBuilder; + +typedef struct +{ + gsize offset; + gsize size; + gpointer data; +} FileChunk; + +static gpointer +file_builder_allocate (FileBuilder *fb, + guint alignment, + gsize size, + struct gvdb_pointer *pointer) +{ + FileChunk *chunk; + + if (size == 0) + return NULL; + + fb->offset += (-fb->offset) & (alignment - 1); + chunk = g_slice_new (FileChunk); + chunk->offset = fb->offset; + chunk->size = size; + chunk->data = g_malloc (size); + + pointer->start = guint32_to_le (fb->offset); + fb->offset += size; + pointer->end = guint32_to_le (fb->offset); + + g_queue_push_tail (fb->chunks, chunk); + + return chunk->data; +} + +static void +file_builder_add_value (FileBuilder *fb, + GVariant *value, + struct gvdb_pointer *pointer) +{ + GVariant *variant, *normal; + gpointer data; + gsize size; + + if (fb->byteswap) + { + value = g_variant_byteswap (value); + variant = g_variant_new_variant (value); + g_variant_unref (value); + } + else + variant = g_variant_new_variant (value); + + normal = g_variant_get_normal_form (variant); + g_variant_unref (variant); + + size = g_variant_get_size (normal); + data = file_builder_allocate (fb, 8, size, pointer); + g_variant_store (normal, data); + g_variant_unref (normal); +} + +static void +file_builder_add_string (FileBuilder *fb, + const gchar *string, + guint32_le *start, + guint16_le *size) +{ + FileChunk *chunk; + gsize length; + + length = strlen (string); + + chunk = g_slice_new (FileChunk); + chunk->offset = fb->offset; + chunk->size = length; + chunk->data = g_malloc (length); + if (length != 0) + memcpy (chunk->data, string, length); + + *start = guint32_to_le (fb->offset); + *size = guint16_to_le (length); + fb->offset += length; + + g_queue_push_tail (fb->chunks, chunk); +} + +static void +file_builder_allocate_for_hash (FileBuilder *fb, + gsize n_buckets, + gsize n_items, + guint bloom_shift, + gsize n_bloom_words, + guint32_le **bloom_filter, + guint32_le **hash_buckets, + struct gvdb_hash_item **hash_items, + struct gvdb_pointer *pointer) +{ + guint32_le bloom_hdr, table_hdr; + guchar *data; + gsize size; + + g_assert (n_bloom_words < (1u << 27)); + + bloom_hdr = guint32_to_le (bloom_shift << 27 | n_bloom_words); + table_hdr = guint32_to_le (n_buckets); + + size = sizeof bloom_hdr + sizeof table_hdr + + n_bloom_words * sizeof (guint32_le) + + n_buckets * sizeof (guint32_le) + + n_items * sizeof (struct gvdb_hash_item); + + data = file_builder_allocate (fb, 4, size, pointer); + +#define chunk(s) (size -= (s), data += (s), data - (s)) + memcpy (chunk (sizeof bloom_hdr), &bloom_hdr, sizeof bloom_hdr); + memcpy (chunk (sizeof table_hdr), &table_hdr, sizeof table_hdr); + *bloom_filter = (guint32_le *) chunk (n_bloom_words * sizeof (guint32_le)); + *hash_buckets = (guint32_le *) chunk (n_buckets * sizeof (guint32_le)); + *hash_items = (struct gvdb_hash_item *) chunk (n_items * + sizeof (struct gvdb_hash_item)); + g_assert (size == 0); +#undef chunk + + memset (*bloom_filter, 0, n_bloom_words * sizeof (guint32_le)); + + /* NOTE - the code to actually fill in the bloom filter here is missing. + * Patches welcome! + * + * http://en.wikipedia.org/wiki/Bloom_filter + * http://0pointer.de/blog/projects/bloom.html + */ +} + +static void +file_builder_add_hash (FileBuilder *fb, + GHashTable *table, + struct gvdb_pointer *pointer) +{ + guint32_le *buckets, *bloom_filter; + struct gvdb_hash_item *items; + HashTable *mytable; + GvdbItem *item; + guint32 index; + gint bucket; + + mytable = hash_table_new (g_hash_table_size (table)); + g_hash_table_foreach (table, hash_table_insert, mytable); + index = 0; + + for (bucket = 0; bucket < mytable->n_buckets; bucket++) + for (item = mytable->buckets[bucket]; item; item = item->next) + item->assigned_index = guint32_to_le (index++); + + file_builder_allocate_for_hash (fb, mytable->n_buckets, index, 5, 0, + &bloom_filter, &buckets, &items, pointer); + + index = 0; + for (bucket = 0; bucket < mytable->n_buckets; bucket++) + { + buckets[bucket] = guint32_to_le (index); + + for (item = mytable->buckets[bucket]; item; item = item->next) + { + struct gvdb_hash_item *entry = items++; + const gchar *basename; + + g_assert (index == guint32_from_le (item->assigned_index)); + entry->hash_value = guint32_to_le (item->hash_value); + entry->parent = item_to_index (item->parent); + entry->unused = 0; + + if (item->parent != NULL) + basename = item->key + strlen (item->parent->key); + else + basename = item->key; + + file_builder_add_string (fb, basename, + &entry->key_start, + &entry->key_size); + + if (item->value != NULL) + { + g_assert (item->child == NULL && item->table == NULL); + + file_builder_add_value (fb, item->value, &entry->value.pointer); + entry->type = 'v'; + } + + if (item->child != NULL) + { + guint32 children = 0, i = 0; + guint32_le *offsets; + GvdbItem *child; + + g_assert (item->table == NULL); + + for (child = item->child; child; child = child->sibling) + children++; + + offsets = file_builder_allocate (fb, 4, 4 * children, + &entry->value.pointer); + entry->type = 'L'; + + for (child = item->child; child; child = child->sibling) + offsets[i++] = child->assigned_index; + + g_assert (children == i); + } + + if (item->table != NULL) + { + entry->type = 'H'; + file_builder_add_hash (fb, item->table, &entry->value.pointer); + } + + index++; + } + } + + hash_table_free (mytable); +} + +static FileBuilder * +file_builder_new (gboolean byteswap) +{ + FileBuilder *builder; + + builder = g_slice_new (FileBuilder); + builder->chunks = g_queue_new (); + builder->offset = sizeof (struct gvdb_header); + builder->byteswap = byteswap; + + return builder; +} + +static GString * +file_builder_serialise (FileBuilder *fb, + struct gvdb_pointer root) +{ + struct gvdb_header header = { { 0, }, }; + GString *result; + + if (fb->byteswap) + { + header.signature[0] = GVDB_SWAPPED_SIGNATURE0; + header.signature[1] = GVDB_SWAPPED_SIGNATURE1; + } + else + { + header.signature[0] = GVDB_SIGNATURE0; + header.signature[1] = GVDB_SIGNATURE1; + } + + result = g_string_new (NULL); + + header.root = root; + g_string_append_len (result, (gpointer) &header, sizeof header); + + while (!g_queue_is_empty (fb->chunks)) + { + FileChunk *chunk = g_queue_pop_head (fb->chunks); + + if (result->len != chunk->offset) + { + gchar zero[8] = { 0, }; + + g_assert (chunk->offset > result->len); + g_assert (chunk->offset - result->len < 8); + + g_string_append_len (result, zero, chunk->offset - result->len); + g_assert (result->len == chunk->offset); + } + + g_string_append_len (result, chunk->data, chunk->size); + g_free (chunk->data); + + g_slice_free (FileChunk, chunk); + } + + g_queue_free (fb->chunks); + g_slice_free (FileBuilder, fb); + + return result; +} + +GBytes * +gvdb_table_get_content (GHashTable *table, + gboolean byteswap) +{ + struct gvdb_pointer root; + FileBuilder *fb; + GString *str; + GBytes *res; + gsize len; + + fb = file_builder_new (byteswap); + file_builder_add_hash (fb, table, &root); + str = file_builder_serialise (fb, root); + + len = str->len; + res = g_bytes_new_take (g_string_free (str, FALSE), len); + + return res; +} + +gboolean +gvdb_table_write_contents (GHashTable *table, + const gchar *filename, + gboolean byteswap, + GError **error) +{ + GBytes *content; + gboolean status; + + content = gvdb_table_get_content (table, byteswap); + + status = g_file_set_contents (filename, g_bytes_get_data (content, NULL), g_bytes_get_size (content), error); + + g_bytes_unref (content); + + return status; +} diff --git a/document-portal/gvdb/gvdb-builder.h b/document-portal/gvdb/gvdb-builder.h new file mode 100644 index 0000000..ad361dc --- /dev/null +++ b/document-portal/gvdb/gvdb-builder.h @@ -0,0 +1,60 @@ +/* + * Copyright © 2010 Codethink Limited + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the licence, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + * + * Author: Ryan Lortie + */ + +#ifndef __gvdb_builder_h__ +#define __gvdb_builder_h__ + +#include + +typedef struct _GvdbItem GvdbItem; + +G_GNUC_INTERNAL +GHashTable * gvdb_hash_table_new (GHashTable *parent, + const gchar *key); + +G_GNUC_INTERNAL +GvdbItem * gvdb_hash_table_insert (GHashTable *table, + const gchar *key); +G_GNUC_INTERNAL +void gvdb_hash_table_insert_string (GHashTable *table, + const gchar *key, + const gchar *value); + +G_GNUC_INTERNAL +void gvdb_item_set_value (GvdbItem *item, + GVariant *value); +G_GNUC_INTERNAL +void gvdb_item_set_hash_table (GvdbItem *item, + GHashTable *table); +G_GNUC_INTERNAL +void gvdb_item_set_parent (GvdbItem *item, + GvdbItem *parent); + +G_GNUC_INTERNAL +gboolean gvdb_table_write_contents (GHashTable *table, + const gchar *filename, + gboolean byteswap, + GError **error); + +G_GNUC_INTERNAL +GBytes * gvdb_table_get_content (GHashTable *table, + gboolean byteswap); + + +#endif /* __gvdb_builder_h__ */ diff --git a/document-portal/gvdb/gvdb-format.h b/document-portal/gvdb/gvdb-format.h new file mode 100644 index 0000000..486e854 --- /dev/null +++ b/document-portal/gvdb/gvdb-format.h @@ -0,0 +1,85 @@ +/* + * Copyright © 2010 Codethink Limited + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the licence, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + * + * Author: Ryan Lortie + */ + +#ifndef __gvdb_format_h__ +#define __gvdb_format_h__ + +#include + +typedef struct { guint16 value; } guint16_le; +typedef struct { guint32 value; } guint32_le; + +struct gvdb_pointer { + guint32_le start; + guint32_le end; +}; + +struct gvdb_hash_header { + guint32_le n_bloom_words; + guint32_le n_buckets; +}; + +struct gvdb_hash_item { + guint32_le hash_value; + guint32_le parent; + + guint32_le key_start; + guint16_le key_size; + gchar type; + gchar unused; + + union + { + struct gvdb_pointer pointer; + gchar direct[8]; + } value; +}; + +struct gvdb_header { + guint32 signature[2]; + guint32_le version; + guint32_le options; + + struct gvdb_pointer root; +}; + +static inline guint32_le guint32_to_le (guint32 value) { + guint32_le result = { GUINT32_TO_LE (value) }; + return result; +} + +static inline guint32 guint32_from_le (guint32_le value) { + return GUINT32_FROM_LE (value.value); +} + +static inline guint16_le guint16_to_le (guint16 value) { + guint16_le result = { GUINT16_TO_LE (value) }; + return result; +} + +static inline guint16 guint16_from_le (guint16_le value) { + return GUINT16_FROM_LE (value.value); +} + +#define GVDB_SIGNATURE0 1918981703 +#define GVDB_SIGNATURE1 1953390953 +#define GVDB_SWAPPED_SIGNATURE0 GUINT32_SWAP_LE_BE (GVDB_SIGNATURE0) +#define GVDB_SWAPPED_SIGNATURE1 GUINT32_SWAP_LE_BE (GVDB_SIGNATURE1) + +#endif /* __gvdb_format_h__ */ diff --git a/document-portal/gvdb/gvdb-reader.c b/document-portal/gvdb/gvdb-reader.c new file mode 100644 index 0000000..08b5bc8 --- /dev/null +++ b/document-portal/gvdb/gvdb-reader.c @@ -0,0 +1,718 @@ +/* + * Copyright © 2010 Codethink Limited + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the licence, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + * + * Author: Ryan Lortie + */ + +#include "gvdb-reader.h" +#include "gvdb-format.h" + +#include + +struct _GvdbTable { + GBytes *bytes; + + const gchar *data; + gsize size; + + gboolean byteswapped; + gboolean trusted; + + const guint32_le *bloom_words; + guint32 n_bloom_words; + guint bloom_shift; + + const guint32_le *hash_buckets; + guint32 n_buckets; + + struct gvdb_hash_item *hash_items; + guint32 n_hash_items; +}; + +static const gchar * +gvdb_table_item_get_key (GvdbTable *file, + const struct gvdb_hash_item *item, + gsize *size) +{ + guint32 start, end; + + start = guint32_from_le (item->key_start); + *size = guint16_from_le (item->key_size); + end = start + *size; + + if G_UNLIKELY (start > end || end > file->size) + return NULL; + + return file->data + start; +} + +static gconstpointer +gvdb_table_dereference (GvdbTable *file, + const struct gvdb_pointer *pointer, + gint alignment, + gsize *size) +{ + guint32 start, end; + + start = guint32_from_le (pointer->start); + end = guint32_from_le (pointer->end); + + if G_UNLIKELY (start > end || end > file->size || start & (alignment - 1)) + return NULL; + + *size = end - start; + + return file->data + start; +} + +static void +gvdb_table_setup_root (GvdbTable *file, + const struct gvdb_pointer *pointer) +{ + const struct gvdb_hash_header *header; + guint32 n_bloom_words; + guint32 n_buckets; + gsize size; + + header = gvdb_table_dereference (file, pointer, 4, &size); + + if G_UNLIKELY (header == NULL || size < sizeof *header) + return; + + size -= sizeof *header; + + n_bloom_words = guint32_from_le (header->n_bloom_words); + n_buckets = guint32_from_le (header->n_buckets); + n_bloom_words &= (1u << 27) - 1; + + if G_UNLIKELY (n_bloom_words * sizeof (guint32_le) > size) + return; + + file->bloom_words = (gpointer) (header + 1); + size -= n_bloom_words * sizeof (guint32_le); + file->n_bloom_words = n_bloom_words; + + if G_UNLIKELY (n_buckets > G_MAXUINT / sizeof (guint32_le) || + n_buckets * sizeof (guint32_le) > size) + return; + + file->hash_buckets = file->bloom_words + file->n_bloom_words; + size -= n_buckets * sizeof (guint32_le); + file->n_buckets = n_buckets; + + if G_UNLIKELY (size % sizeof (struct gvdb_hash_item)) + return; + + file->hash_items = (gpointer) (file->hash_buckets + n_buckets); + file->n_hash_items = size / sizeof (struct gvdb_hash_item); +} + +/** + * gvdb_table_new_from_bytes: + * @bytes: the #GBytes with the data + * @trusted: if the contents of @bytes are trusted + * @error: %NULL, or a pointer to a %NULL #GError + * @returns: a new #GvdbTable + * + * Creates a new #GvdbTable from the contents of @bytes. + * + * This call can fail if the header contained in @bytes is invalid. + * + * You should call gvdb_table_free() on the return result when you no + * longer require it. + **/ +GvdbTable * +gvdb_table_new_from_bytes (GBytes *bytes, + gboolean trusted, + GError **error) +{ + const struct gvdb_header *header; + GvdbTable *file; + + file = g_slice_new0 (GvdbTable); + file->bytes = g_bytes_ref (bytes); + file->data = g_bytes_get_data (bytes, &file->size); + file->trusted = trusted; + + if (file->size < sizeof (struct gvdb_header)) + goto invalid; + + header = (gpointer) file->data; + + if (header->signature[0] == GVDB_SIGNATURE0 && + header->signature[1] == GVDB_SIGNATURE1 && + guint32_from_le (header->version) == 0) + file->byteswapped = FALSE; + + else if (header->signature[0] == GVDB_SWAPPED_SIGNATURE0 && + header->signature[1] == GVDB_SWAPPED_SIGNATURE1 && + guint32_from_le (header->version) == 0) + file->byteswapped = TRUE; + + else + goto invalid; + + gvdb_table_setup_root (file, &header->root); + + return file; + +invalid: + g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "invalid gvdb header"); + + g_bytes_unref (file->bytes); + + g_slice_free (GvdbTable, file); + + return NULL; +} + +/** + * gvdb_table_new: + * @filename: a filename + * @trusted: if the contents of @bytes are trusted + * @error: %NULL, or a pointer to a %NULL #GError + * @returns: a new #GvdbTable + * + * Creates a new #GvdbTable using the #GMappedFile for @filename as the + * #GBytes. + **/ +GvdbTable * +gvdb_table_new (const gchar *filename, + gboolean trusted, + GError **error) +{ + GMappedFile *mapped; + GvdbTable *table; + GBytes *bytes; + + mapped = g_mapped_file_new (filename, FALSE, error); + if (!mapped) + return NULL; + + bytes = g_mapped_file_get_bytes (mapped); + table = gvdb_table_new_from_bytes (bytes, trusted, error); + g_mapped_file_unref (mapped); + g_bytes_unref (bytes); + + g_prefix_error (error, "%s: ", filename); + + return table; +} + +static gboolean +gvdb_table_bloom_filter (GvdbTable *file, + guint32 hash_value) +{ + guint32 word, mask; + + if (file->n_bloom_words == 0) + return TRUE; + + word = (hash_value / 32) % file->n_bloom_words; + mask = 1 << (hash_value & 31); + mask |= 1 << ((hash_value >> file->bloom_shift) & 31); + + return (guint32_from_le (file->bloom_words[word]) & mask) == mask; +} + +static gboolean +gvdb_table_check_name (GvdbTable *file, + struct gvdb_hash_item *item, + const gchar *key, + guint key_length) +{ + const gchar *this_key; + gsize this_size; + guint32 parent; + + this_key = gvdb_table_item_get_key (file, item, &this_size); + + if G_UNLIKELY (this_key == NULL || this_size > key_length) + return FALSE; + + key_length -= this_size; + + if G_UNLIKELY (memcmp (this_key, key + key_length, this_size) != 0) + return FALSE; + + parent = guint32_from_le (item->parent); + if (key_length == 0 && parent == 0xffffffffu) + return TRUE; + + if G_LIKELY (parent < file->n_hash_items && this_size > 0) + return gvdb_table_check_name (file, + &file->hash_items[parent], + key, key_length); + + return FALSE; +} + +static const struct gvdb_hash_item * +gvdb_table_lookup (GvdbTable *file, + const gchar *key, + gchar type) +{ + guint32 hash_value = 5381; + guint key_length; + guint32 bucket; + guint32 lastno; + guint32 itemno; + + if G_UNLIKELY (file->n_buckets == 0 || file->n_hash_items == 0) + return NULL; + + for (key_length = 0; key[key_length]; key_length++) + hash_value = (hash_value * 33) + ((signed char *) key)[key_length]; + + if (!gvdb_table_bloom_filter (file, hash_value)) + return NULL; + + bucket = hash_value % file->n_buckets; + itemno = guint32_from_le (file->hash_buckets[bucket]); + + if (bucket == file->n_buckets - 1 || + (lastno = guint32_from_le(file->hash_buckets[bucket + 1])) > file->n_hash_items) + lastno = file->n_hash_items; + + while G_LIKELY (itemno < lastno) + { + struct gvdb_hash_item *item = &file->hash_items[itemno]; + + if (hash_value == guint32_from_le (item->hash_value)) + if G_LIKELY (gvdb_table_check_name (file, item, key, key_length)) + if G_LIKELY (item->type == type) + return item; + + itemno++; + } + + return NULL; +} + +static gboolean +gvdb_table_list_from_item (GvdbTable *table, + const struct gvdb_hash_item *item, + const guint32_le **list, + guint *length) +{ + gsize size; + + *list = gvdb_table_dereference (table, &item->value.pointer, 4, &size); + + if G_LIKELY (*list == NULL || size % 4) + return FALSE; + + *length = size / 4; + + return TRUE; +} + +/** + * gvdb_table_get_names: + * @table: a #GvdbTable + * @length: the number of items returned, or %NULL + * + * Gets a list of all names contained in @table. + * + * No call to gvdb_table_get_table(), gvdb_table_list() or + * gvdb_table_get_value() will succeed unless it is for one of the + * names returned by this function. + * + * Note that some names that are returned may still fail for all of the + * above calls in the case of the corrupted file. Note also that the + * returned strings may not be utf8. + * + * Returns: a %NULL-terminated list of strings, of length @length + **/ +gchar ** +gvdb_table_get_names (GvdbTable *table, + gint *length) +{ + gchar **names; + gint n_names; + gint filled; + gint total; + gint i; + + /* We generally proceed by iterating over the list of items in the + * hash table (in order of appearance) recording them into an array. + * + * Each item has a parent item (except root items). The parent item + * forms part of the name of the item. We could go fetching the + * parent item chain at the point that we encounter each item but then + * we would need to implement some sort of recursion along with checks + * for self-referential items. + * + * Instead, we do a number of passes. Each pass will build up one + * level of names (starting from the root). We continue to do passes + * until no more items are left. The first pass will only add root + * items and each further pass will only add items whose direct parent + * is an item added in the immediately previous pass. It's also + * possible that items get filled if they follow their parent within a + * particular pass. + * + * At most we will have a number of passes equal to the depth of the + * tree. Self-referential items will never be filled in (since their + * parent will have never been filled in). We continue until we have + * a pass that fills in no additional items. + * + * This takes an O(n) algorithm and turns it into O(n*m) where m is + * the depth of the tree, but in all sane cases the tree won't be very + * deep and the constant factor of this algorithm is lower (and the + * complexity of coding it, as well). + */ + + n_names = table->n_hash_items; + names = g_new0 (gchar *, n_names + 1); + + /* 'names' starts out all-NULL. On each pass we record the number + * of items changed from NULL to non-NULL in 'filled' so we know if we + * should repeat the loop. 'total' counts the total number of items + * filled. If 'total' ends up equal to 'n_names' then we know that + * 'names' has been completely filled. + */ + + total = 0; + do + { + /* Loop until we have filled no more entries */ + filled = 0; + + for (i = 0; i < n_names; i++) + { + const struct gvdb_hash_item *item = &table->hash_items[i]; + const gchar *name; + gsize name_length; + guint32 parent; + + /* already got it on a previous pass */ + if (names[i] != NULL) + continue; + + parent = guint32_from_le (item->parent); + + if (parent == 0xffffffffu) + { + /* it's a root item */ + name = gvdb_table_item_get_key (table, item, &name_length); + + if (name != NULL) + { + names[i] = g_strndup (name, name_length); + filled++; + } + } + + else if (parent < n_names && names[parent] != NULL) + { + /* It's a non-root item whose parent was filled in already. + * + * Calculate the name of this item by combining it with + * its parent name. + */ + name = gvdb_table_item_get_key (table, item, &name_length); + + if (name != NULL) + { + const gchar *parent_name = names[parent]; + gsize parent_length; + gchar *fullname; + + parent_length = strlen (parent_name); + fullname = g_malloc (parent_length + name_length + 1); + memcpy (fullname, parent_name, parent_length); + memcpy (fullname + parent_length, name, name_length); + fullname[parent_length + name_length] = '\0'; + names[i] = fullname; + filled++; + } + } + } + + total += filled; + } + while (filled && total < n_names); + + /* If the table was corrupted then 'names' may have holes in it. + * Collapse those. + */ + if G_UNLIKELY (total != n_names) + { + GPtrArray *fixed_names; + + fixed_names = g_ptr_array_new (); + for (i = 0; i < n_names; i++) + if (names[i] != NULL) + g_ptr_array_add (fixed_names, names[i]); + + g_free (names); + n_names = fixed_names->len; + g_ptr_array_add (fixed_names, NULL); + names = (gchar **) g_ptr_array_free (fixed_names, FALSE); + } + + if (length) + *length = n_names; + + return names; +} + +/** + * gvdb_table_list: + * @file: a #GvdbTable + * @key: a string + * @returns: a %NULL-terminated string array + * + * List all of the keys that appear below @key. The nesting of keys + * within the hash file is defined by the program that created the hash + * file. One thing is constant: each item in the returned array can be + * concatenated to @key to obtain the full name of that key. + * + * It is not possible to tell from this function if a given key is + * itself a path, a value, or another hash table; you are expected to + * know this for yourself. + * + * You should call g_strfreev() on the return result when you no longer + * require it. + **/ +gchar ** +gvdb_table_list (GvdbTable *file, + const gchar *key) +{ + const struct gvdb_hash_item *item; + const guint32_le *list; + gchar **strv; + guint length; + guint i; + + if ((item = gvdb_table_lookup (file, key, 'L')) == NULL) + return NULL; + + if (!gvdb_table_list_from_item (file, item, &list, &length)) + return NULL; + + strv = g_new (gchar *, length + 1); + for (i = 0; i < length; i++) + { + guint32 itemno = guint32_from_le (list[i]); + + if (itemno < file->n_hash_items) + { + const struct gvdb_hash_item *item; + const gchar *string; + gsize strsize; + + item = file->hash_items + itemno; + + string = gvdb_table_item_get_key (file, item, &strsize); + + if (string != NULL) + strv[i] = g_strndup (string, strsize); + else + strv[i] = g_malloc0 (1); + } + else + strv[i] = g_malloc0 (1); + } + + strv[i] = NULL; + + return strv; +} + +/** + * gvdb_table_has_value: + * @file: a #GvdbTable + * @key: a string + * @returns: %TRUE if @key is in the table + * + * Checks for a value named @key in @file. + * + * Note: this function does not consider non-value nodes (other hash + * tables, for example). + **/ +gboolean +gvdb_table_has_value (GvdbTable *file, + const gchar *key) +{ + static const struct gvdb_hash_item *item; + gsize size; + + item = gvdb_table_lookup (file, key, 'v'); + + if (item == NULL) + return FALSE; + + return gvdb_table_dereference (file, &item->value.pointer, 8, &size) != NULL; +} + +static GVariant * +gvdb_table_value_from_item (GvdbTable *table, + const struct gvdb_hash_item *item) +{ + GVariant *variant, *value; + gconstpointer data; + GBytes *bytes; + gsize size; + + data = gvdb_table_dereference (table, &item->value.pointer, 8, &size); + + if G_UNLIKELY (data == NULL) + return NULL; + + bytes = g_bytes_new_from_bytes (table->bytes, ((gchar *) data) - table->data, size); + variant = g_variant_new_from_bytes (G_VARIANT_TYPE_VARIANT, bytes, table->trusted); + value = g_variant_get_variant (variant); + g_variant_unref (variant); + g_bytes_unref (bytes); + + return value; +} + +/** + * gvdb_table_get_value: + * @file: a #GvdbTable + * @key: a string + * @returns: a #GVariant, or %NULL + * + * Looks up a value named @key in @file. + * + * If the value is not found then %NULL is returned. Otherwise, a new + * #GVariant instance is returned. The #GVariant does not depend on the + * continued existence of @file. + * + * You should call g_variant_unref() on the return result when you no + * longer require it. + **/ +GVariant * +gvdb_table_get_value (GvdbTable *file, + const gchar *key) +{ + const struct gvdb_hash_item *item; + GVariant *value; + + if ((item = gvdb_table_lookup (file, key, 'v')) == NULL) + return NULL; + + value = gvdb_table_value_from_item (file, item); + + if (value && file->byteswapped) + { + GVariant *tmp; + + tmp = g_variant_byteswap (value); + g_variant_unref (value); + value = tmp; + } + + return value; +} + +/** + * gvdb_table_get_raw_value: + * @table: a #GvdbTable + * @key: a string + * @returns: a #GVariant, or %NULL + * + * Looks up a value named @key in @file. + * + * This call is equivalent to gvdb_table_get_value() except that it + * never byteswaps the value. + **/ +GVariant * +gvdb_table_get_raw_value (GvdbTable *table, + const gchar *key) +{ + const struct gvdb_hash_item *item; + + if ((item = gvdb_table_lookup (table, key, 'v')) == NULL) + return NULL; + + return gvdb_table_value_from_item (table, item); +} + +/** + * gvdb_table_get_table: + * @file: a #GvdbTable + * @key: a string + * @returns: a new #GvdbTable, or %NULL + * + * Looks up the hash table named @key in @file. + * + * The toplevel hash table in a #GvdbTable can contain reference to + * child hash tables (and those can contain further references...). + * + * If @key is not found in @file then %NULL is returned. Otherwise, a + * new #GvdbTable is returned, referring to the child hashtable as + * contained in the file. This newly-created #GvdbTable does not depend + * on the continued existence of @file. + * + * You should call gvdb_table_free() on the return result when you no + * longer require it. + **/ +GvdbTable * +gvdb_table_get_table (GvdbTable *file, + const gchar *key) +{ + const struct gvdb_hash_item *item; + GvdbTable *new; + + item = gvdb_table_lookup (file, key, 'H'); + + if (item == NULL) + return NULL; + + new = g_slice_new0 (GvdbTable); + new->bytes = g_bytes_ref (file->bytes); + new->byteswapped = file->byteswapped; + new->trusted = file->trusted; + new->data = file->data; + new->size = file->size; + + gvdb_table_setup_root (new, &item->value.pointer); + + return new; +} + +/** + * gvdb_table_free: + * @file: a #GvdbTable + * + * Frees @file. + **/ +void +gvdb_table_free (GvdbTable *file) +{ + g_bytes_unref (file->bytes); + g_slice_free (GvdbTable, file); +} + +/** + * gvdb_table_is_valid: + * @table: a #GvdbTable + * @returns: %TRUE if @table is still valid + * + * Checks if the table is still valid. + * + * An on-disk GVDB can be marked as invalid. This happens when the file + * has been replaced. The appropriate action is typically to reopen the + * file. + **/ +gboolean +gvdb_table_is_valid (GvdbTable *table) +{ + return !!*table->data; +} diff --git a/document-portal/gvdb/gvdb-reader.h b/document-portal/gvdb/gvdb-reader.h new file mode 100644 index 0000000..241b41a --- /dev/null +++ b/document-portal/gvdb/gvdb-reader.h @@ -0,0 +1,63 @@ +/* + * Copyright © 2010 Codethink Limited + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the licence, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + * + * Author: Ryan Lortie + */ + +#ifndef __gvdb_reader_h__ +#define __gvdb_reader_h__ + +#include + +typedef struct _GvdbTable GvdbTable; + +G_BEGIN_DECLS + +G_GNUC_INTERNAL +GvdbTable * gvdb_table_new_from_bytes (GBytes *bytes, + gboolean trusted, + GError **error); +G_GNUC_INTERNAL +GvdbTable * gvdb_table_new (const gchar *filename, + gboolean trusted, + GError **error); +G_GNUC_INTERNAL +void gvdb_table_free (GvdbTable *table); +G_GNUC_INTERNAL +gchar ** gvdb_table_get_names (GvdbTable *table, + gint *length); +G_GNUC_INTERNAL +gchar ** gvdb_table_list (GvdbTable *table, + const gchar *key); +G_GNUC_INTERNAL +GvdbTable * gvdb_table_get_table (GvdbTable *table, + const gchar *key); +G_GNUC_INTERNAL +GVariant * gvdb_table_get_raw_value (GvdbTable *table, + const gchar *key); +G_GNUC_INTERNAL +GVariant * gvdb_table_get_value (GvdbTable *table, + const gchar *key); + +G_GNUC_INTERNAL +gboolean gvdb_table_has_value (GvdbTable *table, + const gchar *key); +G_GNUC_INTERNAL +gboolean gvdb_table_is_valid (GvdbTable *table); + +G_END_DECLS + +#endif /* __gvdb_reader_h__ */ diff --git a/document-portal/gvdb/gvdb.doap b/document-portal/gvdb/gvdb.doap new file mode 100644 index 0000000..b4ae60c --- /dev/null +++ b/document-portal/gvdb/gvdb.doap @@ -0,0 +1,32 @@ + + + + + gvdb + GVariant Database file + + A simple database file format that stores a mapping from strings to + GVariant values in a way that is extremely efficient for lookups. + + The database is written once and can not be modified. + + Included here is reader code and a first-pass implementation of a + writer (that does not currently produce particularly optimised + output). + + It is intended that this code be used by copy-pasting into your + project or by making use of git-merge(1). + + + + + Ryan Lortie + + ryanl + + + + diff --git a/document-portal/meson.build b/document-portal/meson.build new file mode 100644 index 0000000..2f13437 --- /dev/null +++ b/document-portal/meson.build @@ -0,0 +1,98 @@ +permission_store_built_sources = gnome.gdbus_codegen( + 'permission-store-dbus', + sources: '../data/org.freedesktop.impl.portal.PermissionStore.xml', + interface_prefix: 'org.freedesktop.impl.portal', + namespace: 'Xdg', + autocleanup: 'none', +) + +db_sources = files( + 'permission-db.c', + 'gvdb/gvdb-reader.c', + 'gvdb/gvdb-builder.c', +) + +xdg_permission_store_sources = [ + 'permission-store.c', + 'xdg-permission-store.c', + xdp_utils_sources, + db_sources, + sd_escape_sources, + permission_store_built_sources, +] + +xdg_permission_store = executable( + 'xdg-permission-store', + xdg_permission_store_sources, + dependencies: xdg_desktop_portal_deps, + include_directories: incs_xdg_desktop_portal, + install: true, + install_dir: libexecdir, +) + +configure_file( + input: 'xdg-permission-store.service.in', + output: '@BASENAME@', + configuration: base_config, + install: true, + install_dir: systemd_userunit_dir, +) + +document_portal_built_sources = gnome.gdbus_codegen( + 'document-portal-dbus', + sources: [ + '../data/org.freedesktop.portal.Documents.xml', + '../data/org.freedesktop.portal.FileTransfer.xml', + ], + interface_prefix: 'org.freedesktop.portal', + namespace: 'XdpDbus', + autocleanup: 'none', +) + +xdg_document_portal_sources = [ + 'document-portal.c', + 'file-transfer.c', + 'document-store.c', + 'document-portal-fuse.c', + xdp_utils_sources, + db_sources, + sd_escape_sources, + document_portal_built_sources, + permission_store_built_sources, +] + +xdg_document_portal = executable( + 'xdg-document-portal', + xdg_document_portal_sources, + dependencies: [xdg_desktop_portal_deps, fuse3_dep], + include_directories: incs_xdg_desktop_portal, + install: true, + install_dir: libexecdir, +) + +configure_file( + input: 'xdg-document-portal.service.in', + output: '@BASENAME@', + configuration: base_config, + install: true, + install_dir: systemd_userunit_dir, +) + + +doc_portal_service_file_sources = files('org.freedesktop.portal.Documents.service.in') +doc_portal_service_file = configure_file( + input: doc_portal_service_file_sources, + output: '@BASENAME@', + configuration: base_config, + install: true, + install_dir: dbus_service_dir, +) + +permission_portal_service_file_sources = files('org.freedesktop.impl.portal.PermissionStore.service.in') +permission_portal_service_file = configure_file( + input: permission_portal_service_file_sources, + output: '@BASENAME@', + configuration: base_config, + install: true, + install_dir: dbus_service_dir, +) \ No newline at end of file diff --git a/document-portal/org.freedesktop.impl.portal.PermissionStore.service.in b/document-portal/org.freedesktop.impl.portal.PermissionStore.service.in new file mode 100644 index 0000000..cb444f1 --- /dev/null +++ b/document-portal/org.freedesktop.impl.portal.PermissionStore.service.in @@ -0,0 +1,4 @@ +[D-BUS Service] +Name=org.freedesktop.impl.portal.PermissionStore +Exec=@libexecdir@/xdg-permission-store +SystemdService=xdg-permission-store.service diff --git a/document-portal/org.freedesktop.portal.Documents.service.in b/document-portal/org.freedesktop.portal.Documents.service.in new file mode 100644 index 0000000..4d0881b --- /dev/null +++ b/document-portal/org.freedesktop.portal.Documents.service.in @@ -0,0 +1,5 @@ +[D-BUS Service] +Name=org.freedesktop.portal.Documents +Exec=@libexecdir@/xdg-document-portal +SystemdService=xdg-document-portal.service +AssumedAppArmorLabel=unconfined diff --git a/document-portal/permission-db.c b/document-portal/permission-db.c new file mode 100644 index 0000000..c3ca841 --- /dev/null +++ b/document-portal/permission-db.c @@ -0,0 +1,1286 @@ +/* permission-db.c + * + * Copyright (C) 2015 Red Hat, Inc + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Authors: + * Alexander Larsson + */ + +#include "config.h" + +#include +#include +#include +#include +#ifdef HAVE_SYS_STATFS_H +#include +#endif +#ifdef HAVE_SYS_MOUNT_H +#include +#endif + +#include "permission-db.h" +#include "gvdb/gvdb-reader.h" +#include "gvdb/gvdb-builder.h" + +struct PermissionDb +{ + GObject parent; + + char *path; + gboolean fail_if_not_found; + GvdbTable *gvdb; + GBytes *gvdb_contents; + + gboolean dirty; + + /* Map id => GVariant (data, sorted-dict[appid->perms]) */ + GvdbTable *main_table; + GHashTable *main_updates; + + /* (reverse) Map app id => [ id ]*/ + GvdbTable *app_table; + GHashTable *app_additions; + GHashTable *app_removals; +}; + +typedef struct +{ + GObjectClass parent_class; +} PermissionDbClass; + +static void initable_iface_init (GInitableIface *initable_iface); + +G_DEFINE_TYPE_WITH_CODE (PermissionDb, permission_db, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init)); + +enum { + PROP_0, + PROP_PATH, + PROP_FAIL_IF_NOT_FOUND, + LAST_PROP +}; + +static int +cmpstringp (const void *p1, const void *p2) +{ + return strcmp (*(char * const *) p1, *(char * const *) p2); +} + +static void +sort_strv (const char **strv) +{ + qsort (strv, g_strv_length ((char **) strv), sizeof (const char *), cmpstringp); +} + +static int +str_ptr_array_find (GPtrArray *array, + const char *str) +{ + int i; + + for (i = 0; i < array->len; i++) + if (strcmp (g_ptr_array_index (array, i), str) == 0) + return i; + + return -1; +} + +static gboolean +str_ptr_array_contains (GPtrArray *array, + const char *str) +{ + return str_ptr_array_find (array, str) >= 0; +} + +const char * +permission_db_get_path (PermissionDb *self) +{ + g_return_val_if_fail (PERMISSION_IS_DB (self), NULL); + + return self->path; +} + +void +permission_db_set_path (PermissionDb *self, + const char *path) +{ + g_return_if_fail (PERMISSION_IS_DB (self)); + + g_clear_pointer (&self->path, g_free); + self->path = g_strdup (path); +} + +PermissionDb * +permission_db_new (const char *path, + gboolean fail_if_not_found, + GError **error) +{ + return g_initable_new (PERMISSION_TYPE_DB, + NULL, + error, + "path", path, + "fail-if-not-found", fail_if_not_found, + NULL); +} + +static void +permission_db_finalize (GObject *object) +{ + PermissionDb *self = (PermissionDb *) object; + + g_clear_pointer (&self->path, g_free); + g_clear_pointer (&self->gvdb_contents, g_bytes_unref); + g_clear_pointer (&self->gvdb, gvdb_table_free); + g_clear_pointer (&self->main_table, gvdb_table_free); + g_clear_pointer (&self->app_table, gvdb_table_free); + g_clear_pointer (&self->main_updates, g_hash_table_unref); + g_clear_pointer (&self->app_additions, g_hash_table_unref); + g_clear_pointer (&self->app_removals, g_hash_table_unref); + + G_OBJECT_CLASS (permission_db_parent_class)->finalize (object); +} + +static void +permission_db_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + PermissionDb *self = PERMISSION_DB (object); + + switch (prop_id) + { + case PROP_PATH: + g_value_set_string (value, self->path); + break; + + case PROP_FAIL_IF_NOT_FOUND: + g_value_set_boolean (value, self->fail_if_not_found); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +permission_db_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + PermissionDb *self = PERMISSION_DB (object); + + switch (prop_id) + { + case PROP_PATH: + g_clear_pointer (&self->path, g_free); + self->path = g_value_dup_string (value); + break; + + case PROP_FAIL_IF_NOT_FOUND: + self->fail_if_not_found = g_value_get_boolean (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +permission_db_class_init (PermissionDbClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = permission_db_finalize; + object_class->get_property = permission_db_get_property; + object_class->set_property = permission_db_set_property; + + g_object_class_install_property (object_class, + PROP_PATH, + g_param_spec_string ("path", + "", + "", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); + g_object_class_install_property (object_class, + PROP_FAIL_IF_NOT_FOUND, + g_param_spec_boolean ("fail-if-not-found", + "", + "", + TRUE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); +} + +static void +permission_db_init (PermissionDb *self) +{ + self->fail_if_not_found = TRUE; + + self->main_updates = + g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, (GDestroyNotify) permission_db_entry_unref); + self->app_additions = + g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, (GDestroyNotify) g_ptr_array_unref); + self->app_removals = + g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, (GDestroyNotify) g_ptr_array_unref); +} + +static gboolean +is_on_nfs (const char *path) +{ + struct statfs statfs_buffer; + int statfs_result; + g_autofree char *dirname = NULL; + + dirname = g_path_get_dirname (path); + + statfs_result = statfs (dirname, &statfs_buffer); + if (statfs_result != 0) + return FALSE; + + return statfs_buffer.f_type == 0x6969; +} + +static gboolean +initable_init (GInitable *initable, + GCancellable *cancellable, + GError **error) +{ + PermissionDb *self = (PermissionDb *) initable; + GError *my_error = NULL; + + if (self->path == NULL) + return TRUE; + + if (is_on_nfs (self->path)) + { + g_autoptr(GFile) file = g_file_new_for_path (self->path); + char *contents; + gsize length; + + /* We avoid using mmap on NFS, because its prone to give us SIGBUS at semi-random + times (nfs down, file removed, etc). Instead we just load the file */ + if (g_file_load_contents (file, cancellable, &contents, &length, NULL, &my_error)) + self->gvdb_contents = g_bytes_new_take (contents, length); + } + else + { + GMappedFile *mapped = g_mapped_file_new (self->path, FALSE, &my_error); + if (mapped) + { + self->gvdb_contents = g_mapped_file_get_bytes (mapped); + g_mapped_file_unref (mapped); + } + } + + if (self->gvdb_contents == NULL) + { + if (!self->fail_if_not_found && + (g_error_matches (my_error, G_FILE_ERROR, G_FILE_ERROR_NOENT) || + g_error_matches (my_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))) + { + g_error_free (my_error); + } + else + { + g_propagate_error (error, my_error); + return FALSE; + } + } + else + { + self->gvdb = gvdb_table_new_from_bytes (self->gvdb_contents, TRUE, error); + if (self->gvdb == NULL) + return FALSE; + + self->main_table = gvdb_table_get_table (self->gvdb, "main"); + if (self->main_table == NULL) + { + g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, + "No main table in db"); + return FALSE; + } + + self->app_table = gvdb_table_get_table (self->gvdb, "apps"); + if (self->app_table == NULL) + { + g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, + "No app table in db"); + return FALSE; + } + } + + return TRUE; +} + +static void +initable_iface_init (GInitableIface *initable_iface) +{ + initable_iface->init = initable_init; +} + +/* Transfer: full */ +char ** +permission_db_list_ids (PermissionDb *self) +{ + GPtrArray *res; + GHashTableIter iter; + gpointer key, value; + int i; + + g_return_val_if_fail (PERMISSION_IS_DB (self), NULL); + + res = g_ptr_array_new (); + + g_hash_table_iter_init (&iter, self->main_updates); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + if (value != NULL) + g_ptr_array_add (res, g_strdup (key)); + } + + if (self->main_table) + { + // TODO: can we use gvdb_table_list here??? + g_autofree char **main_ids = gvdb_table_get_names (self->main_table, NULL); + + for (i = 0; main_ids[i] != NULL; i++) + { + char *id = main_ids[i]; + + if (g_hash_table_lookup_extended (self->main_updates, id, NULL, NULL)) + g_free (id); + else + g_ptr_array_add (res, id); + } + } + + g_ptr_array_add (res, NULL); + return (char **) g_ptr_array_free (res, FALSE); +} + +static gboolean +app_update_empty (GHashTable *ht, const char *app) +{ + GPtrArray *array; + + array = g_hash_table_lookup (ht, app); + if (array == NULL) + return TRUE; + + return array->len == 0; +} + +/* Transfer: full */ +char ** +permission_db_list_apps (PermissionDb *self) +{ + gpointer key, _value; + GHashTableIter iter; + GPtrArray *res; + int i; + + g_return_val_if_fail (PERMISSION_IS_DB (self), NULL); + + res = g_ptr_array_new (); + + g_hash_table_iter_init (&iter, self->app_additions); + while (g_hash_table_iter_next (&iter, &key, &_value)) + { + GPtrArray *value = _value; + if (value->len > 0) + g_ptr_array_add (res, g_strdup (key)); + } + + if (self->app_table) + { + // TODO: can we use gvdb_table_list here??? + g_autofree char **apps = gvdb_table_get_names (self->app_table, NULL); + + for (i = 0; apps[i] != NULL; i++) + { + char *app = apps[i]; + gboolean empty = TRUE; + GPtrArray *removals; + int j; + + /* Don't use if we already added above */ + if (app_update_empty (self->app_additions, app)) + { + g_autoptr(GVariant) ids_v = NULL; + + removals = g_hash_table_lookup (self->app_removals, app); + + /* Add unless all items are removed */ + ids_v = gvdb_table_get_value (self->app_table, app); + + if (ids_v) + { + g_autofree const char **ids = g_variant_get_strv (ids_v, NULL); + + for (j = 0; ids[j] != NULL; j++) + { + if (removals == NULL || + !str_ptr_array_contains (removals, ids[j])) + { + empty = FALSE; + break; + } + } + } + } + + if (empty) + g_free (app); + else + g_ptr_array_add (res, app); + } + } + + g_ptr_array_add (res, NULL); + return (char **) g_ptr_array_free (res, FALSE); +} + +/* Transfer: full */ +char ** +permission_db_list_ids_by_app (PermissionDb *self, + const char *app) +{ + GPtrArray *res; + GPtrArray *additions; + GPtrArray *removals; + int i; + + g_return_val_if_fail (PERMISSION_IS_DB (self), NULL); + + res = g_ptr_array_new (); + + additions = g_hash_table_lookup (self->app_additions, app); + removals = g_hash_table_lookup (self->app_removals, app); + + if (additions) + { + for (i = 0; i < additions->len; i++) + g_ptr_array_add (res, + g_strdup (g_ptr_array_index (additions, i))); + } + + if (self->app_table) + { + g_autoptr(GVariant) ids_v = gvdb_table_get_value (self->app_table, app); + if (ids_v) + { + g_autofree const char **ids = g_variant_get_strv (ids_v, NULL); + + for (i = 0; ids[i] != NULL; i++) + { + if (removals == NULL || + !str_ptr_array_contains (removals, ids[i])) + g_ptr_array_add (res, g_strdup (ids[i])); + } + } + } + + g_ptr_array_add (res, NULL); + return (char **) g_ptr_array_free (res, FALSE); +} + +/* Transfer: full */ +PermissionDbEntry * +permission_db_lookup (PermissionDb *self, + const char *id) +{ + GVariant *res = NULL; + gpointer value; + + g_return_val_if_fail (PERMISSION_IS_DB (self), NULL); + g_return_val_if_fail (id != NULL, NULL); + + if (g_hash_table_lookup_extended (self->main_updates, id, NULL, &value)) + { + if (value != NULL) + res = g_variant_ref ((GVariant *) value); + } + else if (self->main_table) + { + res = gvdb_table_get_value (self->main_table, id); + } + + return (PermissionDbEntry *) res; +} + +/* Transfer: full */ +char ** +permission_db_list_ids_by_value (PermissionDb *self, + GVariant *data) +{ + g_autofree char **ids = permission_db_list_ids (self); + int i; + GPtrArray *res; + + g_return_val_if_fail (PERMISSION_IS_DB (self), NULL); + g_return_val_if_fail (data != NULL, NULL); + + res = g_ptr_array_new (); + + for (i = 0; ids[i] != NULL; i++) + { + char *id = ids[i]; + + g_autoptr(PermissionDbEntry) entry = NULL; + g_autoptr(GVariant) entry_data = NULL; + + entry = permission_db_lookup (self, id); + if (entry) + { + entry_data = permission_db_entry_get_data (entry); + if (g_variant_equal (data, entry_data)) + { + g_ptr_array_add (res, id); + id = NULL; /* Don't free, as we return this */ + } + } + g_free (id); + } + + g_ptr_array_add (res, NULL); + return (char **) g_ptr_array_free (res, FALSE); +} + +static void +add_app_id (PermissionDb *self, + const char *app, + const char *id) +{ + GPtrArray *additions; + GPtrArray *removals; + int i; + + additions = g_hash_table_lookup (self->app_additions, app); + removals = g_hash_table_lookup (self->app_removals, app); + + if (removals) + { + i = str_ptr_array_find (removals, id); + if (i >= 0) + g_ptr_array_remove_index_fast (removals, i); + } + + if (additions) + { + if (!str_ptr_array_contains (additions, id)) + g_ptr_array_add (additions, g_strdup (id)); + } + else + { + additions = g_ptr_array_new_with_free_func (g_free); + g_ptr_array_add (additions, g_strdup (id)); + g_hash_table_insert (self->app_additions, + g_strdup (app), additions); + } +} + +static void +remove_app_id (PermissionDb *self, + const char *app, + const char *id) +{ + GPtrArray *additions; + GPtrArray *removals; + int i; + + additions = g_hash_table_lookup (self->app_additions, app); + removals = g_hash_table_lookup (self->app_removals, app); + + if (additions) + { + i = str_ptr_array_find (additions, id); + if (i >= 0) + g_ptr_array_remove_index_fast (additions, i); + } + + if (removals) + { + if (!str_ptr_array_contains (removals, id)) + g_ptr_array_add (removals, g_strdup (id)); + } + else + { + removals = g_ptr_array_new_with_free_func (g_free); + g_ptr_array_add (removals, g_strdup (id)); + g_hash_table_insert (self->app_removals, + g_strdup (app), removals); + } +} + +gboolean +permission_db_is_dirty (PermissionDb *self) +{ + g_return_val_if_fail (PERMISSION_IS_DB (self), FALSE); + + return self->dirty; +} + +/* add, replace, or NULL entry to remove */ +void +permission_db_set_entry (PermissionDb *self, + const char *id, + PermissionDbEntry *entry) +{ + g_autoptr(PermissionDbEntry) old_entry = NULL; + g_autofree const char **old = NULL; + g_autofree const char **new = NULL; + static const char *empty[] = { NULL }; + const char **a, **b; + int ia, ib; + + g_return_if_fail (PERMISSION_IS_DB (self)); + g_return_if_fail (id != NULL); + + self->dirty = TRUE; + + old_entry = permission_db_lookup (self, id); + + g_hash_table_insert (self->main_updates, + g_strdup (id), + permission_db_entry_ref (entry)); + + a = empty; + b = empty; + + if (old_entry) + { + old = permission_db_entry_list_apps (old_entry); + sort_strv (old); + a = old; + } + + if (entry) + { + new = permission_db_entry_list_apps (entry); + sort_strv (new); + b = new; + } + + ia = 0; + ib = 0; + while (a[ia] != NULL || b[ib] != NULL) + { + if (a[ia] == NULL) + { + /* Not in old, but in new => added */ + add_app_id (self, b[ib], id); + ib++; + } + else if (b[ib] == NULL) + { + /* Not in new, but in old => removed */ + remove_app_id (self, a[ia], id); + ia++; + } + else + { + int cmp = strcmp (a[ia], b[ib]); + + if (cmp == 0) + { + /* In both, no change */ + ia++; + ib++; + } + else if (cmp < 0) + { + /* Not in new, but in old => removed */ + remove_app_id (self, a[ia], id); + ia++; + } + else /* cmp > 0 */ + { + /* Not in old, but in new => added */ + add_app_id (self, b[ib], id); + ib++; + } + } + } +} + +void +permission_db_update (PermissionDb *self) +{ + GHashTable *root, *main_h, *apps_h; + GBytes *new_contents; + GvdbTable *new_gvdb; + int i; + + g_auto(GStrv) ids = NULL; + g_auto(GStrv) apps = NULL; + + g_return_if_fail (PERMISSION_IS_DB (self)); + + root = gvdb_hash_table_new (NULL, NULL); + main_h = gvdb_hash_table_new (root, "main"); + apps_h = gvdb_hash_table_new (root, "apps"); + g_hash_table_unref (main_h); + g_hash_table_unref (apps_h); + + ids = permission_db_list_ids (self); + for (i = 0; ids[i] != 0; i++) + { + g_autoptr(PermissionDbEntry) entry = permission_db_lookup (self, ids[i]); + if (entry != NULL) + { + GvdbItem *item; + + item = gvdb_hash_table_insert (main_h, ids[i]); + gvdb_item_set_value (item, (GVariant *) entry); + } + } + + apps = permission_db_list_apps (self); + for (i = 0; apps[i] != 0; i++) + { + g_auto(GStrv) app_ids = permission_db_list_ids_by_app (self, apps[i]); + GVariantBuilder builder; + GvdbItem *item; + int j; + + /* May as well ensure that on-disk arrays are sorted, even if we don't use it yet */ + sort_strv ((const char **) app_ids); + + /* We should never list an app that has empty id lists */ + g_assert (app_ids[0] != NULL); + + g_variant_builder_init (&builder, G_VARIANT_TYPE_ARRAY); + for (j = 0; app_ids[j] != NULL; j++) + g_variant_builder_add (&builder, "s", app_ids[j]); + + item = gvdb_hash_table_insert (apps_h, apps[i]); + gvdb_item_set_value (item, g_variant_builder_end (&builder)); + } + + new_contents = gvdb_table_get_content (root, FALSE); + new_gvdb = gvdb_table_new_from_bytes (new_contents, TRUE, NULL); + + /* This was just created, any failure to parse it is purely an internal error */ + g_assert (new_gvdb != NULL); + + g_clear_pointer (&self->gvdb_contents, g_bytes_unref); + g_clear_pointer (&self->gvdb, gvdb_table_free); + self->gvdb_contents = new_contents; + self->gvdb = new_gvdb; + self->dirty = FALSE; +} + +GBytes * +permission_db_get_content (PermissionDb *self) +{ + g_return_val_if_fail (PERMISSION_IS_DB (self), NULL); + + return self->gvdb_contents; +} + +/* Note: You must first call update to serialize, this only saves serialied data */ +gboolean +permission_db_save_content (PermissionDb *self, + GError **error) +{ + GBytes *content = NULL; + + if (self->gvdb_contents == NULL) + { + g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, + "No content to save"); + return FALSE; + } + + if (self->path == NULL) + { + g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, + "No path set"); + return FALSE; + } + + content = self->gvdb_contents; + return g_file_set_contents (self->path, g_bytes_get_data (content, NULL), g_bytes_get_size (content), error); +} + +static void +save_content_callback (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GTask) task = user_data; + GFile *file = G_FILE (source_object); + gboolean ok; + g_autoptr(GError) error = NULL; + + ok = g_file_replace_contents_finish (file, + res, + NULL, &error); + if (ok) + g_task_return_boolean (task, TRUE); + else + g_task_return_error (task, error); +} + +void +permission_db_save_content_async (PermissionDb *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GBytes *content = NULL; + + g_autoptr(GTask) task = NULL; + g_autoptr(GFile) file = NULL; + + task = g_task_new (self, cancellable, callback, user_data); + + if (self->gvdb_contents == NULL) + { + g_task_return_new_error (task, G_FILE_ERROR, G_FILE_ERROR_INVAL, + "No content to save"); + return; + } + + if (self->path == NULL) + { + g_task_return_new_error (task, G_FILE_ERROR, G_FILE_ERROR_INVAL, + "No path set"); + return; + } + + content = g_bytes_ref (self->gvdb_contents); + g_task_set_task_data (task, content, (GDestroyNotify) g_bytes_unref); + + file = g_file_new_for_path (self->path); + g_file_replace_contents_bytes_async (file, content, + NULL, FALSE, 0, + cancellable, + save_content_callback, + g_object_ref (task)); +} + +gboolean +permission_db_save_content_finish (PermissionDb *self, + GAsyncResult *res, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (res), error); +} + + +GString * +permission_db_print_string (PermissionDb *self, + GString *string) +{ + g_auto(GStrv) ids = NULL; + g_auto(GStrv) apps = NULL; + int i; + + g_return_val_if_fail (PERMISSION_IS_DB (self), NULL); + + if G_UNLIKELY (string == NULL) + string = g_string_new (NULL); + + g_string_append_printf (string, "main {\n"); + + ids = permission_db_list_ids (self); + sort_strv ((const char **) ids); + for (i = 0; ids[i] != 0; i++) + { + g_autoptr(PermissionDbEntry) entry = permission_db_lookup (self, ids[i]); + g_string_append_printf (string, " %s: ", ids[i]); + if (entry != NULL) + permission_db_entry_print_string (entry, string); + g_string_append_printf (string, "\n"); + } + + g_string_append_printf (string, "}\napps {\n"); + + apps = permission_db_list_apps (self); + sort_strv ((const char **) apps); + for (i = 0; apps[i] != 0; i++) + { + int j; + g_auto(GStrv) app_ids = NULL; + + app_ids = permission_db_list_ids_by_app (self, apps[i]); + sort_strv ((const char **) app_ids); + + g_string_append_printf (string, " %s: ", apps[i]); + for (j = 0; app_ids[j] != NULL; j++) + g_string_append_printf (string, "%s%s", j == 0 ? "" : ", ", app_ids[j]); + g_string_append_printf (string, "\n"); + } + + g_string_append_printf (string, "}\n"); + + return string; +} + +char * +permission_db_print (PermissionDb *self) +{ + return g_string_free (permission_db_print_string (self, NULL), FALSE); +} + +PermissionDbEntry * +permission_db_entry_ref (PermissionDbEntry *entry) +{ + if (entry != NULL) + g_variant_ref ((GVariant *) entry); + return entry; +} + +void +permission_db_entry_unref (PermissionDbEntry *entry) +{ + if (entry != NULL) + g_variant_unref ((GVariant *) entry); +} + +/* Transfer: full */ +GVariant * +permission_db_entry_get_data (PermissionDbEntry *entry) +{ + g_autoptr(GVariant) variant = g_variant_get_child_value ((GVariant *) entry, 0); + + return g_variant_get_child_value (variant, 0); +} + +/* Transfer: container */ +const char ** +permission_db_entry_list_apps (PermissionDbEntry *entry) +{ + GVariant *v = (GVariant *) entry; + + g_autoptr(GVariant) app_array = NULL; + GVariantIter iter; + GVariant *child; + GPtrArray *res; + + res = g_ptr_array_new (); + + app_array = g_variant_get_child_value (v, 1); + + g_variant_iter_init (&iter, app_array); + while ((child = g_variant_iter_next_value (&iter))) + { + const char *child_app_id; + g_autoptr(GVariant) permissions = g_variant_get_child_value (child, 1); + + if (g_variant_n_children (permissions) > 0) + { + g_variant_get_child (child, 0, "&s", &child_app_id); + g_ptr_array_add (res, (char *) child_app_id); + } + + g_variant_unref (child); + } + + g_ptr_array_add (res, NULL); + return (const char **) g_ptr_array_free (res, FALSE); +} + +static GVariant * +permission_db_entry_get_permissions_variant (PermissionDbEntry *entry, + const char *app_id) +{ + GVariant *v = (GVariant *) entry; + + g_autoptr(GVariant) app_array = NULL; + GVariant *child; + GVariant *res = NULL; + gsize n_children, start, end, m; + const char *child_app_id; + int cmp; + + app_array = g_variant_get_child_value (v, 1); + + n_children = g_variant_n_children (app_array); + + start = 0; + end = n_children; + while (start < end) + { + m = (start + end) / 2; + + child = g_variant_get_child_value (app_array, m); + g_variant_get_child (child, 0, "&s", &child_app_id); + + cmp = strcmp (app_id, child_app_id); + if (cmp == 0) + { + res = g_variant_get_child_value (child, 1); + break; + } + else if (cmp < 0) + { + end = m; + } + else /* cmp > 0 */ + { + start = m + 1; + } + } + + return res; +} + +gboolean +permission_db_entry_has_app (PermissionDbEntry *entry, + const char *app) +{ + g_autoptr(GVariant) permissions = NULL; + + permissions = permission_db_entry_get_permissions_variant (entry, app); + + return permissions != NULL; +} + + +/* Transfer: container */ +const char ** +permission_db_entry_list_permissions (PermissionDbEntry *entry, + const char *app) +{ + g_autoptr(GVariant) permissions = NULL; + + permissions = permission_db_entry_get_permissions_variant (entry, app); + if (permissions) + return g_variant_get_strv (permissions, NULL); + else + return g_new0 (const char *, 1); +} + +gboolean +permission_db_entry_has_permission (PermissionDbEntry *entry, + const char *app, + const char *permission) +{ + g_autofree const char **app_permissions = NULL; + + app_permissions = permission_db_entry_list_permissions (entry, app); + + return g_strv_contains (app_permissions, permission); +} + +gboolean +permission_db_entry_has_permissions (PermissionDbEntry *entry, + const char *app, + const char **permissions) +{ + g_autofree const char **app_permissions = NULL; + int i; + + app_permissions = permission_db_entry_list_permissions (entry, app); + + for (i = 0; permissions[i] != NULL; i++) + { + if (!g_strv_contains (app_permissions, permissions[i])) + return FALSE; + } + + return TRUE; +} + +static GVariant * +make_entry (GVariant *data, + GVariant *app_permissions) +{ + return g_variant_new ("(v@a{sas})", data, app_permissions); +} + +static GVariant * +make_empty_app_permissions (void) +{ + return g_variant_new_array (G_VARIANT_TYPE ("{sas}"), NULL, 0); +} + +static GVariant * +make_permissions (const char *app, const char **permissions) +{ + static const char **empty = { NULL }; + + if (permissions == NULL) + permissions = empty; + + return g_variant_new ("{s@as}", + app, + g_variant_new_strv (permissions, -1)); +} + +static GVariant * +add_permissions (GVariant *app_permissions, + GVariant *permissions) +{ + GVariantBuilder builder; + GVariantIter iter; + GVariant *child; + gboolean added = FALSE; + int cmp; + const char *new_app_id; + const char *child_app_id; + + g_autoptr(GVariant) new_perms_array = NULL; + + g_variant_get (permissions, "{&s@as}", &new_app_id, &new_perms_array); + + g_variant_builder_init (&builder, G_VARIANT_TYPE_ARRAY); + + /* Insert or replace permissions in sorted order */ + + g_variant_iter_init (&iter, app_permissions); + while ((child = g_variant_iter_next_value (&iter))) + { + g_autoptr(GVariant) old_perms_array = NULL; + + g_variant_get (child, "{&s@as}", &child_app_id, &old_perms_array); + + cmp = strcmp (new_app_id, child_app_id); + if (cmp == 0) + { + added = TRUE; + /* Replace old permissions */ + g_variant_builder_add_value (&builder, permissions); + } + else if (cmp < 0) + { + if (!added) + { + added = TRUE; + g_variant_builder_add_value (&builder, permissions); + } + g_variant_builder_add_value (&builder, child); + } + else /* cmp > 0 */ + { + g_variant_builder_add_value (&builder, child); + } + + g_variant_unref (child); + } + + if (!added) + g_variant_builder_add_value (&builder, permissions); + + return g_variant_builder_end (&builder); +} + +static GVariant * +remove_permissions (GVariant *app_permissions, + const char *app) +{ + GVariantBuilder builder; + GVariantIter iter; + GVariant *child; + const char *child_app_id; + + g_variant_builder_init (&builder, G_VARIANT_TYPE_ARRAY); + + g_variant_iter_init (&iter, app_permissions); + while ((child = g_variant_iter_next_value (&iter))) + { + g_autoptr(GVariant) old_perms_array = NULL; + + g_variant_get (child, "{&s@as}", &child_app_id, &old_perms_array); + + if (strcmp (app, child_app_id) != 0) + g_variant_builder_add_value (&builder, child); + } + + return g_variant_builder_end (&builder); +} + +PermissionDbEntry * +permission_db_entry_new (GVariant *data) +{ + GVariant *res; + + if (data == NULL) + data = g_variant_new_byte (0); + + res = make_entry (data, + make_empty_app_permissions ()); + + return (PermissionDbEntry *) g_variant_ref_sink (res); +} + +PermissionDbEntry * +permission_db_entry_modify_data (PermissionDbEntry *entry, + GVariant *data) +{ + GVariant *v = (GVariant *) entry; + GVariant *res; + + if (data == NULL) + data = g_variant_new_byte (0); + + res = make_entry (data, + g_variant_get_child_value (v, 1)); + return (PermissionDbEntry *) g_variant_ref_sink (res); +} + +PermissionDbEntry * +permission_db_entry_set_app_permissions (PermissionDbEntry *entry, + const char *app, + const char **permissions) +{ + GVariant *v = (GVariant *) entry; + GVariant *res; + + g_autoptr(GVariant) old_data_v = g_variant_get_child_value (v, 0); + g_autoptr(GVariant) old_data = g_variant_get_child_value (old_data_v, 0); + g_autoptr(GVariant) old_permissions = g_variant_get_child_value (v, 1); + + res = make_entry (old_data, + add_permissions (old_permissions, + make_permissions (app, + permissions))); + return (PermissionDbEntry *) g_variant_ref_sink (res); +} + +PermissionDbEntry * +permission_db_entry_remove_app_permissions (PermissionDbEntry *entry, + const char *app) +{ + GVariant *v = (GVariant *) entry; + GVariant *res; + GVariant *app_permissions; + + g_autoptr(GVariant) old_data_v = g_variant_get_child_value (v, 0); + g_autoptr(GVariant) old_data = g_variant_get_child_value (old_data_v, 0); + g_autoptr(GVariant) old_permissions = g_variant_get_child_value (v, 1); + + app_permissions = remove_permissions (old_permissions, app); + if (app_permissions == NULL) + app_permissions = make_empty_app_permissions (); + + res = make_entry (old_data, app_permissions); + return (PermissionDbEntry *) g_variant_ref_sink (res); +} + +GString * +permission_db_entry_print_string (PermissionDbEntry *entry, + GString *string) +{ + return g_variant_print_string ((GVariant *) entry, string, FALSE); +} diff --git a/document-portal/permission-db.h b/document-portal/permission-db.h new file mode 100644 index 0000000..6c02a07 --- /dev/null +++ b/document-portal/permission-db.h @@ -0,0 +1,104 @@ +/* permission-db.h + * + * Copyright © 2015 Red Hat, Inc + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Authors: + * Alexander Larsson + */ + +#ifndef PERMISSION_DB_H +#define PERMISSION_DB_H + +#include + +#include + +G_BEGIN_DECLS + +typedef struct PermissionDb PermissionDb; +typedef struct _PermissionDbEntry PermissionDbEntry; + +#define PERMISSION_TYPE_DB (permission_db_get_type ()) +#define PERMISSION_DB(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), PERMISSION_TYPE_DB, PermissionDb)) +#define PERMISSION_IS_DB(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), PERMISSION_TYPE_DB)) + +GType permission_db_get_type (void); + +PermissionDb * permission_db_new (const char *path, + gboolean fail_if_not_found, + GError **error); +char ** permission_db_list_ids (PermissionDb *self); +char ** permission_db_list_apps (PermissionDb *self); +char ** permission_db_list_ids_by_app (PermissionDb *self, + const char *app); +char ** permission_db_list_ids_by_value (PermissionDb *self, + GVariant *data); +PermissionDbEntry *permission_db_lookup (PermissionDb *self, + const char *id); +GString * permission_db_print_string (PermissionDb *self, + GString *string); +char * permission_db_print (PermissionDb *self); + +gboolean permission_db_is_dirty (PermissionDb *self); +void permission_db_set_entry (PermissionDb *self, + const char *id, + PermissionDbEntry *entry); +void permission_db_update (PermissionDb *self); +GBytes * permission_db_get_content (PermissionDb *self); +const char * permission_db_get_path (PermissionDb *self); +gboolean permission_db_save_content (PermissionDb *self, + GError **error); +void permission_db_save_content_async (PermissionDb *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean permission_db_save_content_finish (PermissionDb *self, + GAsyncResult *res, + GError **error); +void permission_db_set_path (PermissionDb *self, + const char *path); + + +PermissionDbEntry *permission_db_entry_ref (PermissionDbEntry *entry); +void permission_db_entry_unref (PermissionDbEntry *entry); +GVariant * permission_db_entry_get_data (PermissionDbEntry *entry); +const char ** permission_db_entry_list_apps (PermissionDbEntry *entry); +const char ** permission_db_entry_list_permissions (PermissionDbEntry *entry, + const char *app); +gboolean permission_db_entry_has_permission (PermissionDbEntry *entry, + const char *app, + const char *permission); +gboolean permission_db_entry_has_permissions (PermissionDbEntry *entry, + const char *app, + const char **permissions); +GString * permission_db_entry_print_string (PermissionDbEntry *entry, + GString *string); + +PermissionDbEntry *permission_db_entry_new (GVariant *data); +PermissionDbEntry *permission_db_entry_modify_data (PermissionDbEntry *entry, + GVariant *data); +PermissionDbEntry *permission_db_entry_set_app_permissions (PermissionDbEntry *entry, + const char *app, + const char **permissions); +PermissionDbEntry *permission_db_entry_remove_app_permissions (PermissionDbEntry *entry, + const char *app); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (PermissionDb, g_object_unref) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (PermissionDbEntry, permission_db_entry_unref) + +G_END_DECLS + +#endif /* PERMISSION_DB_H */ diff --git a/document-portal/permission-store.c b/document-portal/permission-store.c new file mode 100644 index 0000000..e1106d3 --- /dev/null +++ b/document-portal/permission-store.c @@ -0,0 +1,140 @@ +/* + * Copyright © 2014 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Alexander Larsson + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include "glib-backports.h" +#include "permission-store-dbus.h" +#include "xdg-permission-store.h" + +static void +on_bus_acquired (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + xdg_permission_store_start (connection); +} + +static void +on_name_acquired (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ +} + +static void +on_name_lost (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + g_debug ("Name lost."); + exit (1); +} + +static gboolean opt_verbose; +static gboolean opt_replace; +static gboolean opt_version; + +static GOptionEntry entries[] = { + { "verbose", 'v', 0, G_OPTION_ARG_NONE, &opt_verbose, "Print debug information", NULL }, + { "replace", 'r', 0, G_OPTION_ARG_NONE, &opt_replace, "Replace", NULL }, + { "version", 0, 0, G_OPTION_ARG_NONE, &opt_version, "Print version and exit", NULL }, + { NULL } +}; + +static void +message_handler (const gchar *log_domain, + GLogLevelFlags log_level, + const gchar *message, + gpointer user_data) +{ + /* Make this look like normal console output */ + if (log_level & G_LOG_LEVEL_DEBUG) + printf ("XDP: %s\n", message); + else + printf ("%s: %s\n", g_get_prgname (), message); +} + +static void +printerr_handler (const gchar *string) +{ + fprintf (stderr, "error: %s\n", string); +} + +int +main (int argc, + char **argv) +{ + guint owner_id; + GMainLoop *loop; + GOptionContext *context; + g_autoptr(GError) error = NULL; + + g_log_writer_default_set_use_stderr (TRUE); + + setlocale (LC_ALL, ""); + + g_setenv ("GIO_USE_VFS", "local", TRUE); + + g_set_prgname (argv[0]); + + g_set_printerr_handler (printerr_handler); + + context = g_option_context_new ("- permission store"); + g_option_context_add_main_entries (context, entries, NULL); + if (!g_option_context_parse (context, &argc, &argv, &error)) + { + g_printerr ("Option parsing failed: %s", error->message); + return 1; + } + + if (opt_version) + { + g_print ("%s\n", PACKAGE_STRING); + exit (EXIT_SUCCESS); + } + + if (opt_verbose) + g_log_set_handler (G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, message_handler, NULL); + + g_set_prgname (argv[0]); + + owner_id = g_bus_own_name (G_BUS_TYPE_SESSION, + "org.freedesktop.impl.portal.PermissionStore", + G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | (opt_replace ? G_BUS_NAME_OWNER_FLAGS_REPLACE : 0), + + on_bus_acquired, + on_name_acquired, + on_name_lost, + NULL, + NULL); + + loop = g_main_loop_new (NULL, FALSE); + g_main_loop_run (loop); + + g_bus_unown_name (owner_id); + + return 0; +} diff --git a/document-portal/xdg-document-portal.service.in b/document-portal/xdg-document-portal.service.in new file mode 100644 index 0000000..2c90589 --- /dev/null +++ b/document-portal/xdg-document-portal.service.in @@ -0,0 +1,9 @@ +[Unit] +Description=flatpak document portal service +PartOf=graphical-session.target + +[Service] +BusName=org.freedesktop.portal.Documents +ExecStart=@libexecdir@/xdg-document-portal +Type=dbus +Slice=session.slice diff --git a/document-portal/xdg-permission-store.c b/document-portal/xdg-permission-store.c new file mode 100644 index 0000000..a18a4d8 --- /dev/null +++ b/document-portal/xdg-permission-store.c @@ -0,0 +1,541 @@ +/* + * Copyright © 2015 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Alexander Larsson + */ + +#include "config.h" + +#include +#include +#include +#include +#include "permission-store-dbus.h" +#include "xdg-permission-store.h" +#include "permission-db.h" +#include "src/xdp-utils.h" + +GHashTable *tables = NULL; + +typedef struct +{ + char *name; + PermissionDb *db; + GList *outstanding_writes; + GList *current_writes; + gboolean writing; +} Table; + +static void start_writeout (Table *table); + +static void +table_free (Table *table) +{ + g_free (table->name); + g_object_unref (table->db); + g_free (table); +} + +static Table * +lookup_table (const char *name, + GDBusMethodInvocation *invocation) +{ + Table *table; + PermissionDb *db; + g_autofree char *dir = NULL; + g_autofree char *path = NULL; + + g_autoptr(GError) error = NULL; + + table = g_hash_table_lookup (tables, name); + if (table != NULL) + return table; + + dir = g_build_filename (g_get_user_data_dir (), "flatpak/db", NULL); + g_mkdir_with_parents (dir, 0755); + + path = g_build_filename (dir, name, NULL); + db = permission_db_new (path, FALSE, &error); + if (db == NULL) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_FAILED, + "Unable to load db file %s: %s", name, error->message); + return NULL; + } + + table = g_new0 (Table, 1); + table->name = g_strdup (name); + table->db = db; + + g_hash_table_insert (tables, table->name, table); + + return table; +} + +static void +writeout_done (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + Table *table = user_data; + GList *l; + + g_autoptr(GError) error = NULL; + gboolean ok; + + ok = permission_db_save_content_finish (table->db, res, &error); + + for (l = table->current_writes; l != NULL; l = l->next) + { + GDBusMethodInvocation *invocation = l->data; + + if (ok) + g_dbus_method_invocation_return_value (invocation, + g_variant_new ("()")); + else + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_FAILED, + "Unable to write db: %s", error->message); + } + + g_list_free (table->current_writes); + table->current_writes = NULL; + table->writing = FALSE; + + if (table->outstanding_writes != NULL) + start_writeout (table); +} + +static void +start_writeout (Table *table) +{ + g_assert (table->current_writes == NULL); + table->current_writes = table->outstanding_writes; + table->outstanding_writes = NULL; + table->writing = TRUE; + + permission_db_update (table->db); + + permission_db_save_content_async (table->db, NULL, writeout_done, table); +} + +static void +ensure_writeout (Table *table, + GDBusMethodInvocation *invocation) +{ + table->outstanding_writes = g_list_prepend (table->outstanding_writes, invocation); + + if (!table->writing) + start_writeout (table); +} + +static gboolean +handle_list (XdgPermissionStore *object, + GDBusMethodInvocation *invocation, + const gchar *table_name) +{ + Table *table; + + g_auto(GStrv) ids = NULL; + + table = lookup_table (table_name, invocation); + if (table == NULL) + return TRUE; + + ids = permission_db_list_ids (table->db); + + xdg_permission_store_complete_list (object, invocation, (const char * const *) ids); + + return TRUE; +} + +static GVariant * +get_app_permissions (PermissionDbEntry *entry) +{ + g_autofree const char **apps = NULL; + GVariantBuilder builder; + int i; + + apps = permission_db_entry_list_apps (entry); + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sas}")); + + for (i = 0; apps[i] != NULL; i++) + { + g_autofree const char **permissions = permission_db_entry_list_permissions (entry, apps[i]); + g_variant_builder_add_value (&builder, + g_variant_new ("{s@as}", + apps[i], + g_variant_new_strv (permissions, -1))); + } + + return g_variant_ref_sink (g_variant_builder_end (&builder)); +} + +static gboolean +handle_lookup (XdgPermissionStore *object, + GDBusMethodInvocation *invocation, + const gchar *table_name, + const gchar *id) +{ + Table *table; + + g_autoptr(GVariant) data = NULL; + g_autoptr(GVariant) permissions = NULL; + g_autoptr(PermissionDbEntry) entry = NULL; + + table = lookup_table (table_name, invocation); + if (table == NULL) + return TRUE; + + entry = permission_db_lookup (table->db, id); + if (entry == NULL) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_FOUND, + "No entry for %s", id); + return TRUE; + } + + data = permission_db_entry_get_data (entry); + permissions = get_app_permissions (entry); + + xdg_permission_store_complete_lookup (object, invocation, + permissions, + g_variant_new_variant (data)); + + return TRUE; +} + +static void +emit_deleted (XdgPermissionStore *object, + const gchar *table_name, + const gchar *id, + PermissionDbEntry *entry) +{ + g_autoptr(GVariant) data = NULL; + g_autoptr(GVariant) permissions = NULL; + + data = permission_db_entry_get_data (entry); + permissions = g_variant_ref_sink (g_variant_new_array (G_VARIANT_TYPE ("{sas}"), NULL, 0)); + + xdg_permission_store_emit_changed (object, + table_name, id, + TRUE, + g_variant_new_variant (data), + permissions); +} + + +static void +emit_changed (XdgPermissionStore *object, + const gchar *table_name, + const gchar *id, + PermissionDbEntry *entry) +{ + g_autoptr(GVariant) data = NULL; + g_autoptr(GVariant) permissions = NULL; + + data = permission_db_entry_get_data (entry); + permissions = get_app_permissions (entry); + + xdg_permission_store_emit_changed (object, + table_name, id, + FALSE, + g_variant_new_variant (data), + permissions); +} + +static gboolean +handle_delete (XdgPermissionStore *object, + GDBusMethodInvocation *invocation, + const gchar *table_name, + const gchar *id) +{ + Table *table; + + g_autoptr(PermissionDbEntry) entry = NULL; + + table = lookup_table (table_name, invocation); + if (table == NULL) + return TRUE; + + entry = permission_db_lookup (table->db, id); + if (entry == NULL) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_FOUND, + "No entry for %s", id); + return TRUE; + } + + permission_db_set_entry (table->db, id, NULL); + emit_deleted (object, table_name, id, entry); + + ensure_writeout (table, invocation); + + return TRUE; +} + +static gboolean +handle_delete_permission (XdgPermissionStore *object, + GDBusMethodInvocation *invocation, + const char *table_name, + const char *id, + const char *app) +{ + Table *table; + + g_autoptr(PermissionDbEntry) entry = NULL; + g_autoptr(PermissionDbEntry) new_entry = NULL; + + table = lookup_table (table_name, invocation); + if (table == NULL) + return TRUE; + + entry = permission_db_lookup (table->db, id); + if (entry == NULL) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_FOUND, + "No entry for %s", id); + return TRUE; + } + + new_entry = permission_db_entry_remove_app_permissions (entry, app); + permission_db_set_entry (table->db, id, new_entry); + emit_changed (object, table_name, id, new_entry); + + ensure_writeout (table, invocation); + + return TRUE; +} + +static gboolean +handle_get_permission (XdgPermissionStore *object, + GDBusMethodInvocation *invocation, + const char *table_name, + const char *id, + const char *app) +{ + Table *table; + + g_autoptr(PermissionDbEntry) entry = NULL; + g_autofree const char **permission = NULL; + + table = lookup_table (table_name, invocation); + if (table == NULL) + return TRUE; + + entry = permission_db_lookup (table->db, id); + if (entry == NULL) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_FOUND, + "No entry for %s", id); + return TRUE; + } + + permission = permission_db_entry_list_permissions (entry, app); + + xdg_permission_store_complete_get_permission (object, invocation, permission); + + return TRUE; +} + +static gboolean +handle_set (XdgPermissionStore *object, + GDBusMethodInvocation *invocation, + const gchar *table_name, + gboolean create, + const gchar *id, + GVariant *app_permissions, + GVariant *data) +{ + Table *table; + GVariantIter iter; + GVariant *child; + + g_autoptr(GVariant) data_child = NULL; + g_autoptr(PermissionDbEntry) old_entry = NULL; + g_autoptr(PermissionDbEntry) new_entry = NULL; + + table = lookup_table (table_name, invocation); + if (table == NULL) + return TRUE; + + old_entry = permission_db_lookup (table->db, id); + if (old_entry == NULL && !create) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_FOUND, + "Id %s not found", id); + return TRUE; + } + + data_child = g_variant_get_child_value (data, 0); + new_entry = permission_db_entry_new (data_child); + + /* Add all the given app permissions */ + + g_variant_iter_init (&iter, app_permissions); + while ((child = g_variant_iter_next_value (&iter))) + { + g_autoptr(PermissionDbEntry) old_entry = NULL; + const char *child_app_id; + g_autofree const char **permissions; + + g_variant_get (child, "{&s^a&s}", &child_app_id, &permissions); + + old_entry = new_entry; + new_entry = permission_db_entry_set_app_permissions (new_entry, child_app_id, (const char **) permissions); + + g_variant_unref (child); + } + + permission_db_set_entry (table->db, id, new_entry); + emit_changed (object, table_name, id, new_entry); + + ensure_writeout (table, invocation); + + return TRUE; +} + +static gboolean +handle_set_permission (XdgPermissionStore *object, + GDBusMethodInvocation *invocation, + const gchar *table_name, + gboolean create, + const gchar *id, + const gchar *app, + const gchar *const *permissions) +{ + Table *table; + + g_autoptr(PermissionDbEntry) entry = NULL; + g_autoptr(PermissionDbEntry) new_entry = NULL; + + table = lookup_table (table_name, invocation); + if (table == NULL) + return TRUE; + + entry = permission_db_lookup (table->db, id); + if (entry == NULL) + { + if (create) + { + entry = permission_db_entry_new (NULL); + } + else + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_FOUND, + "Id %s not found", id); + return TRUE; + } + } + + new_entry = permission_db_entry_set_app_permissions (entry, app, (const char **) permissions); + permission_db_set_entry (table->db, id, new_entry); + emit_changed (object, table_name, id, new_entry); + + ensure_writeout (table, invocation); + + return TRUE; +} + +static gboolean +handle_set_value (XdgPermissionStore *object, + GDBusMethodInvocation *invocation, + const gchar *table_name, + gboolean create, + const gchar *id, + GVariant *data) +{ + Table *table; + + g_autoptr(GVariant) data_child = NULL; + g_autoptr(PermissionDbEntry) entry = NULL; + g_autoptr(PermissionDbEntry) new_entry = NULL; + + table = lookup_table (table_name, invocation); + if (table == NULL) + return TRUE; + + data_child = g_variant_get_child_value (data, 0); + + entry = permission_db_lookup (table->db, id); + if (entry == NULL) + { + if (create) + { + new_entry = permission_db_entry_new (data_child); + } + else + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_FOUND, + "Id %s not found", id); + return TRUE; + } + } + else + { + new_entry = permission_db_entry_modify_data (entry, data_child); + } + + permission_db_set_entry (table->db, id, new_entry); + emit_changed (object, table_name, id, new_entry); + + ensure_writeout (table, invocation); + + return TRUE; +} + +void +xdg_permission_store_start (GDBusConnection *connection) +{ + XdgPermissionStore *store; + GError *error = NULL; + + g_debug ("Starting permission store"); + + tables = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, (GDestroyNotify) table_free); + + store = xdg_permission_store_skeleton_new (); + + xdg_permission_store_set_version (XDG_PERMISSION_STORE (store), 2); + + g_signal_connect (store, "handle-list", G_CALLBACK (handle_list), NULL); + g_signal_connect (store, "handle-lookup", G_CALLBACK (handle_lookup), NULL); + g_signal_connect (store, "handle-set", G_CALLBACK (handle_set), NULL); + g_signal_connect (store, "handle-set-permission", G_CALLBACK (handle_set_permission), NULL); + g_signal_connect (store, "handle-set-value", G_CALLBACK (handle_set_value), NULL); + g_signal_connect (store, "handle-delete", G_CALLBACK (handle_delete), NULL); + g_signal_connect (store, "handle-delete-permission", G_CALLBACK (handle_delete_permission), NULL); + g_signal_connect (store, "handle-get-permission", G_CALLBACK (handle_get_permission), NULL); + + if (!g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (store), + connection, + "/org/freedesktop/impl/portal/PermissionStore", + &error)) + { + g_warning ("error: %s", error->message); + g_error_free (error); + } +} diff --git a/document-portal/xdg-permission-store.h b/document-portal/xdg-permission-store.h new file mode 100644 index 0000000..6d47c75 --- /dev/null +++ b/document-portal/xdg-permission-store.h @@ -0,0 +1,23 @@ +/* + * Copyright © 2015 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Alexander Larsson + */ + +#pragma once + +void xdg_permission_store_start (GDBusConnection *connection); diff --git a/document-portal/xdg-permission-store.service.in b/document-portal/xdg-permission-store.service.in new file mode 100644 index 0000000..e26c2b9 --- /dev/null +++ b/document-portal/xdg-permission-store.service.in @@ -0,0 +1,9 @@ +[Unit] +Description=sandboxed app permission store +PartOf=graphical-session.target + +[Service] +BusName=org.freedesktop.impl.portal.PermissionStore +ExecStart=@libexecdir@/xdg-permission-store +Type=dbus +Slice=session.slice diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..b25f9ef --- /dev/null +++ b/meson.build @@ -0,0 +1,193 @@ +project( + 'xdg-desktop-portal', + 'c', + version: '1.17.0', + meson_version: '>= 0.56.2', + license: 'LGPL-2.0-or-later', + default_options: ['warning_level=2']) + +###### various directories we'll use later +# foodir are built-in ones, foo_dir are our options + +prefix = get_option('prefix') +datadir = prefix / get_option('datadir') +libexecdir = prefix / get_option('libexecdir') +sysconfdir = prefix / get_option('sysconfdir') +localedir = prefix / get_option('localedir') +dbus_service_dir = get_option('dbus-service-dir') +if dbus_service_dir == '' + dbus_service_dir = prefix / datadir / 'dbus-1' / 'services' +endif + +flatpak_intf_dir = get_option('flatpak-interfaces-dir') +if flatpak_intf_dir == '' + flatpak_required = host_machine.system() in ['linux'] + flatpak_dep = dependency('flatpak', version: '>= 1.5.0', required : flatpak_required) + if flatpak_dep.found() + flatpak_intf_dir = flatpak_dep.get_variable(pkgconfig: 'interfaces_dir') + endif +endif + +systemd_userunit_dir = get_option('systemd-user-unit-dir') +if systemd_userunit_dir == '' + # This is deliberately not ${libdir}: systemd units always go in + # .../lib, never .../lib64 or .../lib/x86_64-linux-gnu + systemd_userunit_dir = prefix / 'lib' / 'systemd' / 'user' +endif + +dataroot_dir = get_option('datarootdir') +if dataroot_dir == '' + dataroot_dir = datadir +endif + +installed_tests_dir = prefix / libexecdir / 'installed-tests' / meson.project_name() +installed_tests_data_dir = prefix / datadir / 'installed-tests' / meson.project_name() +docs_dir = datadir / 'doc' / meson.project_name() + +summary({ + 'DBus service dir': dbus_service_dir, + 'Flatpak interfaces dir': flatpak_intf_dir, + 'systemd user unit dir': systemd_userunit_dir, + 'Installed tests dir': installed_tests_dir, + }, + section: 'Directories', +) + +###### various include directories we'll use later +# These are set here so meson handles the relative paths correctly, +# makes life easier for us + +common_includes = include_directories('.') # config.h +src_includes = include_directories('src') + +###### plugins, dependencies, compiler setup + +i18n = import('i18n') +gnome = import('gnome') +pkgconfig = import('pkgconfig') + +cc = meson.get_compiler('c') +cflags = [ + '-Wno-unused-parameter', + '-Wno-sign-compare', + '-Wno-missing-field-initializers', +] +add_project_arguments(cc.get_supported_arguments(cflags), language : 'c') + +config_h = configuration_data() +config_h.set('_GNU_SOURCE', 1) +config_h.set_quoted('G_LOG_DOMAIN', 'xdg-desktop-portal') +config_h.set_quoted('DATADIR', datadir) +config_h.set_quoted('LIBEXECDIR', libexecdir) +config_h.set_quoted('LOCALEDIR', localedir) +config_h.set_quoted('SYSCONFDIR', sysconfdir) +config_h.set_quoted('GETTEXT_PACKAGE', 'xdg-desktop-portal') +config_h.set_quoted('PACKAGE_STRING', 'xdg-desktop-portal @0@'.format(meson.project_version())) +if cc.has_function('renameat2') + config_h.set('HAVE_RENAMEAT2', 1) +endif + +check_headers = [ + ['sys/vfs.h', 'HAVE_SYS_VFS_H'], + ['sys/mount.h', 'HAVE_SYS_MOUNT_H'], + ['sys/statfs.h', 'HAVE_SYS_STATFS_H'], + ['sys/xattr.h', 'HAVE_SYS_XATTR_H'], + ['sys/extattr.h', 'HAVE_SYS_EXTATTR_H'], +] + +foreach h : check_headers + config_h.set(h.get(1), cc.has_header(h.get(0))) +endforeach + +glib_dep = dependency('glib-2.0', version: '>= 2.66') +gio_dep = dependency('gio-2.0') +gio_unix_dep = dependency('gio-unix-2.0') +json_glib_dep = dependency('json-glib-1.0') +fuse3_dep = dependency('fuse3', version: '>= 3.10.0') +gdk_pixbuf_dep = dependency('gdk-pixbuf-2.0') +geoclue_dep = dependency('libgeoclue-2.0', + version: '>= 2.5.2', + required: get_option('geoclue')) +libportal_dep = dependency('libportal', + required: get_option('libportal')) +pipewire_dep = dependency('libpipewire-0.3', version: '>= 0.2.90') +libsystemd_dep = dependency('libsystemd', required: get_option('systemd')) + +bwrap_required = host_machine.system() in ['linux'] +bwrap = find_program('bwrap', required: bwrap_required) + +have_libportal = libportal_dep.found() +if have_libportal + config_h.set('HAVE_LIBPORTAL', 1) +endif + +have_geoclue = geoclue_dep.found() +if have_geoclue + config_h.set('HAVE_GEOCLUE', 1) +endif + +have_libsystemd = libsystemd_dep.found() +if have_libsystemd + config_h.set('HAVE_LIBSYSTEMD', 1) +endif + +add_project_arguments(['-DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_66'], language: 'c') + +build_docbook = false +xmlto = find_program('xmlto', required: get_option('docbook-docs')) +if xmlto.found() + fs = import('fs') + # we're going to copy this file in to our build tree + if fs.is_file(flatpak_intf_dir / 'org.freedesktop.portal.Flatpak.xml') + build_docbook = true + elif get_option('docbook-docs').enabled() + error('Flatpak development files are required to build DocBook docs') + endif +endif + +enable_installed_tests = get_option('installed-tests') + +###### systemd units, dbus service files, pkgconfig + +base_config = configuration_data() +base_config.set('prefix', prefix) +base_config.set('datadir', datadir) +base_config.set('datarootdir', dataroot_dir) +base_config.set('libexecdir', libexecdir) +base_config.set('VERSION', meson.project_version()) +base_config.set('extraargs', '') + +pkgconfig = import('pkgconfig') +pkgconfig.generate( + name: 'xdg-desktop-portal', + description: 'Desktop integration portal', + dataonly: true, + variables: { + 'prefix': get_option('prefix'), + 'datarootdir': dataroot_dir, + 'datadir': '${prefix}/@0@'.format(get_option('datadir')), + 'interfaces_dir': '${datadir}/dbus-1/interfaces/', + }, +) + +subdir('data') +subdir('src') +subdir('document-portal') +subdir('tests') +subdir('po') +subdir('doc') + +###### generate config.h +configure_file(output: 'config.h', configuration: config_h) + +summary({ + 'Enable docbook documentation': build_docbook, + 'Enable libsystemd support': have_libsystemd, + 'Enable geoclue support': have_geoclue, + 'Enable libportal support': have_libportal, + 'Enable installed tests:': enable_installed_tests, + 'Enable python test suite': enable_pytest, + }, + section: 'Optional builds', + bool_yn: true, +) diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..fccada3 --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,44 @@ +option('dbus-service-dir', + type: 'string', + value: '', + description: 'directory for dbus service files (default: PREFIX/share/dbus-1/services)') +option('flatpak-interfaces-dir', + type: 'string', + value: '', + description: 'directory for Flatpak interface files (default: PREFIX/share/dbus-1/interfaces)') +option('systemd-user-unit-dir', + type: 'string', + value: '', + description: 'directory for systemd user service files (default: PREFIX/lib/systemd/user)') +option('libportal', + type: 'feature', + value: 'auto', + description: 'Enable libportal support. Needed for tests') +option('geoclue', + type: 'feature', + value: 'auto', + description: 'Enable Geoclue support. Needed for location portal') +option('systemd', + type: 'feature', + value: 'auto', + description: 'Enable systemd support') +option('docbook-docs', + type: 'feature', + value: 'auto', + description: 'Build documentation (requires xmlto)') +option('xmlto-flags', + type: 'array', + value: [], + description: 'Define/override "xmlto" options, like "--skip-validation"') +option('datarootdir', + type: 'string', + value: '', + description: 'Define the datarootdir for the pkgconf file') +option('installed-tests', + type: 'boolean', + value: false, + description: 'Enable installation of some test cases') +option('pytest', + type: 'feature', + value: 'auto', + description: 'Enable the pytest-based test suite') diff --git a/po/LINGUAS b/po/LINGUAS new file mode 100644 index 0000000..9c847f6 --- /dev/null +++ b/po/LINGUAS @@ -0,0 +1,31 @@ +be +ca +cs +da +de +en_GB +es +fr +gl +he +hi +hr +hu +id +it +ka +lt +nl +oc +pl +pt +pt_BR +ro +ru +sk +sr +sv +tr +uk +zh_CN +zh_TW diff --git a/po/POTFILES.in b/po/POTFILES.in new file mode 100644 index 0000000..5729dfe --- /dev/null +++ b/po/POTFILES.in @@ -0,0 +1,7 @@ +# List of source files containing translatable strings. +src/background.c +src/device.c +src/location.c +src/screenshot.c +src/settings.c +src/wallpaper.c diff --git a/po/be.po b/po/be.po new file mode 100644 index 0000000..41855d1 --- /dev/null +++ b/po/be.po @@ -0,0 +1,185 @@ +# Belarusian translation for xdg-desktop-portal. +# Copyright (C) 2023 xdg-desktop-portal's COPYRIGHT HOLDER +# This file is distributed under the same license as the xdg-desktop-portal package. +# Yuras Shumovich , 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: xdg-desktop-portal main\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-04 16:59-0300\n" +"PO-Revision-Date: 2023-01-04 01:42+0300\n" +"Last-Translator: Yuras Shumovich \n" +"Language-Team: Belarusian \n" +"Language: be\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Generator: Poedit 3.2.2\n" + +#: src/background.c:741 +#, c-format +msgid "Allow %s to run in the background?" +msgstr "Дазволіць %s выкананне ў фоне?" + +#: src/background.c:745 +#, c-format +msgid "%s requests to be started automatically and run in the background." +msgstr "%s хоча атрымаць доступ на аўтаматычны запуск і выкананне ў фоне." + +#: src/background.c:747 +#, c-format +msgid "%s requests to run in the background." +msgstr "%s хоча атрымаць доступ на выкананне ў фоне." + +#: src/background.c:748 +msgid "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." +msgstr "" +"Дазвол на «выкананне ў фоне» заўсёды можна змяніць праз налады праграм." + +#: src/background.c:753 +msgid "Don't allow" +msgstr "Не дазваляць" + +#: src/background.c:754 src/screenshot.c:238 src/wallpaper.c:182 +msgid "Allow" +msgstr "Дазволіць" + +#: src/device.c:116 +msgid "Turn On Microphone?" +msgstr "Уключыць мікрафон?" + +#: src/device.c:117 +msgid "" +"Access to your microphone can be changed at any time from the privacy " +"settings." +msgstr "Доступ да мікрафона заўсёды можна змяніць праз налады прыватнасці." + +#: src/device.c:121 +msgid "An application wants to use your microphone." +msgstr "Праграма хоча атрымаць доступ да мікрафона." + +#: src/device.c:123 +#, c-format +msgid "%s wants to use your microphone." +msgstr "%s хоча атрымаць доступ да мікрафона." + +#: src/device.c:129 +msgid "Turn On Speakers?" +msgstr "Уключыць дынамікі?" + +#: src/device.c:130 +msgid "" +"Access to your speakers can be changed at any time from the privacy settings." +msgstr "Доступ да дынамікаў заўсёды можна змяніць праз налады прыватнасці." + +#: src/device.c:134 +msgid "An application wants to play sound." +msgstr "Праграма хоча атрымаць доступ на прайграванне гуку." + +#: src/device.c:136 +#, c-format +msgid "%s wants to play sound." +msgstr "%s хоча атрымаць доступ на прайграванне гуку." + +#: src/device.c:142 +msgid "Turn On Camera?" +msgstr "Уключыць камеру?" + +#: src/device.c:143 +msgid "" +"Access to your camera can be changed at any time from the privacy settings." +msgstr "Доступ да камеры заўсёды можна змяніць праз налады прыватнасці." + +#: src/device.c:147 +msgid "An application wants to use your camera." +msgstr "Праграма хоча атрымаць доступ да камеры." + +#: src/device.c:149 +#, c-format +msgid "%s wants to use your camera." +msgstr "%s хоча атрымаць доступ да камеры." + +#: src/location.c:526 +msgid "Deny Access" +msgstr "Забараніць доступ" + +#: src/location.c:528 +msgid "Grant Access" +msgstr "Дазволіць доступ" + +#: src/location.c:534 +msgid "Grant Access to Your Location?" +msgstr "Дазволіць доступ да вашага месцазнаходжання?" + +#: src/location.c:535 +msgid "An application wants to use your location." +msgstr "Праграма хоча атрымаць доступ да месцазнаходжання." + +#: src/location.c:547 +#, c-format +msgid "Give %s Access to Your Location?" +msgstr "Даць %s доступ да вашага месцазнаходжання?" + +#: src/location.c:551 +#, c-format +msgid "%s wants to use your location." +msgstr "%s хоча атрымаць доступ да месцазнаходжання." + +#: src/location.c:554 +msgid "Location access can be changed at any time from the privacy settings." +msgstr "" +"Доступ да месцазнаходжанне заўсёды можна змяніць праз налады прыватнасці." + +#: src/screenshot.c:236 src/wallpaper.c:180 +msgid "Deny" +msgstr "Забараніць" + +#: src/screenshot.c:252 +#, c-format +msgid "Allow %s to Take Screenshots?" +msgstr "Дазволіць %s рабіць здымкі экрана?" + +#: src/screenshot.c:253 +#, c-format +msgid "%s wants to be able to take screenshots at any time." +msgstr "%s хоча атрымаць доступ у любы час рабіць здымкі экрана." + +#: src/screenshot.c:261 +msgid "Allow Applications to Take Screenshots?" +msgstr "Дазволіць праграмам рабіць здымкі экрана?" + +#: src/screenshot.c:262 +msgid "An application wants to be able to take screenshots at any time." +msgstr "Праграма хоча атрымаць доступ у любы час рабіць здымкі экрана." + +#: src/screenshot.c:265 src/wallpaper.c:209 +msgid "This permission can be changed at any time from the privacy settings." +msgstr "" +"Доступ да месцазнаходжанне заўсёды можна змяніць праз налады прыватнасці." + +#: src/settings.c:127 +msgid "Requested setting not found" +msgstr "Запытаная налада не знойдзена" + +#: src/wallpaper.c:192 +msgid "Allow Applications to Set Backgrounds?" +msgstr "Дазволіць праграмам змяняць фон?" + +#: src/wallpaper.c:193 +msgid "An application is requesting to be able to change the background image." +msgstr "Праграма запытвае доступ на змяненне фонавай выявы." + +#: src/wallpaper.c:205 +#, c-format +msgid "Allow %s to Set Backgrounds?" +msgstr "Дазволіць %s змяняць фон?" + +#: src/wallpaper.c:206 +#, c-format +msgid "%s is requesting to be able to change the background image." +msgstr "%s хоча атрымаць доступ на змяненне фонавай выявы." diff --git a/po/ca.po b/po/ca.po new file mode 100644 index 0000000..7839432 --- /dev/null +++ b/po/ca.po @@ -0,0 +1,194 @@ +# Catalan translation for xdg-desktop-portal. +# Copyright (C) 2021 xdg-desktop-portal's COPYRIGHT HOLDER +# This file is distributed under the same license as the xdg-desktop-portal package. +# Maite Guix , 2021. +# Jordi Mas i Hernàndez , 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: xdg-desktop-portal master\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-04 16:59-0300\n" +"PO-Revision-Date: 2021-11-10 19:00+0100\n" +"Last-Translator: Jordi Mas i Hernàndez \n" +"Language-Team: Catalan \n" +"Language: ca\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 3.0\n" + +#: src/background.c:741 +#, c-format +msgid "Allow %s to run in the background?" +msgstr "Voleu que %s s'executi en segon pla?" + +#: src/background.c:745 +#, c-format +msgid "%s requests to be started automatically and run in the background." +msgstr "" +"%s sol·licituds que s'inicien automàticament i s'executen en segon pla." + +#: src/background.c:747 +#, c-format +msgid "%s requests to run in the background." +msgstr "%s sol·licituds per a executar-se en segon pla." + +#: src/background.c:748 +msgid "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." +msgstr "" +"El permís «executa en segon pla» es pot canviar en qualsevol moment des de " +"configuració de l'aplicació." + +#: src/background.c:753 +msgid "Don't allow" +msgstr "No ho permetis" + +#: src/background.c:754 src/screenshot.c:238 src/wallpaper.c:182 +msgid "Allow" +msgstr "Permet" + +#: src/device.c:116 +msgid "Turn On Microphone?" +msgstr "Voleu activar el micròfon?" + +#: src/device.c:117 +msgid "" +"Access to your microphone can be changed at any time from the privacy " +"settings." +msgstr "" +"L'accés al micròfon es pot canviar en qualsevol moment des de la " +"configuració de privacitat." + +#: src/device.c:121 +msgid "An application wants to use your microphone." +msgstr "Una aplicació vol utilitzar el micròfon." + +#: src/device.c:123 +#, c-format +msgid "%s wants to use your microphone." +msgstr "%s vol utilitzar el micròfon." + +#: src/device.c:129 +msgid "Turn On Speakers?" +msgstr "Voleu activar els altaveus?" + +#: src/device.c:130 +msgid "" +"Access to your speakers can be changed at any time from the privacy settings." +msgstr "" +"L'accés als altaveus es pot canviar en qualsevol moment des de la " +"configuració de privacitat." + +#: src/device.c:134 +msgid "An application wants to play sound." +msgstr "Una aplicació vol reproduir so." + +#: src/device.c:136 +#, c-format +msgid "%s wants to play sound." +msgstr "%s vol reproduir so." + +#: src/device.c:142 +msgid "Turn On Camera?" +msgstr "Voleu activar la càmera?" + +#: src/device.c:143 +msgid "" +"Access to your camera can be changed at any time from the privacy settings." +msgstr "" +"L'accés a la càmera es pot canviar en qualsevol moment des de la " +"configuració de privacitat." + +#: src/device.c:147 +msgid "An application wants to use your camera." +msgstr "Una aplicació vol utilitzar la càmera." + +#: src/device.c:149 +#, c-format +msgid "%s wants to use your camera." +msgstr "%s vol utilitzar la càmera." + +#: src/location.c:526 +msgid "Deny Access" +msgstr "Denega l'accés" + +#: src/location.c:528 +msgid "Grant Access" +msgstr "Permet l'accés" + +#: src/location.c:534 +msgid "Grant Access to Your Location?" +msgstr "Permeteu l'accés a la vostra ubicació?" + +#: src/location.c:535 +msgid "An application wants to use your location." +msgstr "Una aplicació vol utilitzar la teva ubicació." + +#: src/location.c:547 +#, c-format +msgid "Give %s Access to Your Location?" +msgstr "Permeteu que %s tingui accés a la vostra ubicació?" + +#: src/location.c:551 +#, c-format +msgid "%s wants to use your location." +msgstr "%s vol saber la teva ubicació." + +#: src/location.c:554 +msgid "Location access can be changed at any time from the privacy settings." +msgstr "" +"Podeu canviar la configuració de l'accés a la ubicació sempre que vulgueu " +"des de la configuració de la privacitat." + +#: src/screenshot.c:236 src/wallpaper.c:180 +msgid "Deny" +msgstr "Denega" + +#: src/screenshot.c:252 +#, c-format +msgid "Allow %s to Take Screenshots?" +msgstr "Voleu permetre que %s faci captures de pantalla?" + +#: src/screenshot.c:253 +#, c-format +msgid "%s wants to be able to take screenshots at any time." +msgstr "%s vol poder fer captures de pantalla en qualsevol moment." + +#: src/screenshot.c:261 +msgid "Allow Applications to Take Screenshots?" +msgstr "Voleu permetre que les aplicacions facin captures de pantalla?" + +#: src/screenshot.c:262 +msgid "An application wants to be able to take screenshots at any time." +msgstr "Una aplicació vol poder fer captures de pantalla en qualsevol moment." + +#: src/screenshot.c:265 src/wallpaper.c:209 +msgid "This permission can be changed at any time from the privacy settings." +msgstr "" +"Aquest permís es pot canviar en qualsevol moment des de la configuració de " +"la privacitat." + +#: src/settings.c:127 +msgid "Requested setting not found" +msgstr "No s'ha trobat la configuració sol·licitada" + +#: src/wallpaper.c:192 +msgid "Allow Applications to Set Backgrounds?" +msgstr "Voleu permetre que les aplicacions estableixin el fons?" + +#: src/wallpaper.c:193 +msgid "An application is requesting to be able to change the background image." +msgstr "Una aplicació sol·licita poder canviar la imatge de fons." + +#: src/wallpaper.c:205 +#, c-format +msgid "Allow %s to Set Backgrounds?" +msgstr "Voleu permetre que %s defineixi els fons?" + +#: src/wallpaper.c:206 +#, c-format +msgid "%s is requesting to be able to change the background image." +msgstr "%s demana poder canviar la imatge de fons." diff --git a/po/cs.po b/po/cs.po new file mode 100644 index 0000000..314d973 --- /dev/null +++ b/po/cs.po @@ -0,0 +1,188 @@ +# Czech translation for xdg-desktop-portal. +# Copyright (C) 2016 xdg-desktop-portal's COPYRIGHT HOLDER +# This file is distributed under the same license as the xdg-desktop-portal package. +# +# Marek Černocký , 2016. +# Daniel Rusek , 2018. +# +msgid "" +msgstr "" +"Project-Id-Version: xdg-desktop-portal master\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-04 16:59-0300\n" +"PO-Revision-Date: 2022-09-20 23:38+0200\n" +"Last-Translator: Daniel Rusek \n" +"Language-Team: Czech \n" +"Language: cs\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" +"X-Generator: Poedit 3.1.1\n" + +#: src/background.c:741 +#, c-format +msgid "Allow %s to run in the background?" +msgstr "Povolit aplikaci %s běh na pozadí?" + +#: src/background.c:745 +#, c-format +msgid "%s requests to be started automatically and run in the background." +msgstr "Aplikace %s požaduje automatické spouštění a běh na pozadí." + +#: src/background.c:747 +#, c-format +msgid "%s requests to run in the background." +msgstr "Aplikace %s požaduje běh na pozadí." + +#: src/background.c:748 +msgid "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." +msgstr "" +"Oprávnění pro „běh na pozadí“ můžete kdykoliv změnit v nastavení aplikace." + +#: src/background.c:753 +msgid "Don't allow" +msgstr "Nepovolit" + +#: src/background.c:754 src/screenshot.c:238 src/wallpaper.c:182 +msgid "Allow" +msgstr "Povolit" + +#: src/device.c:116 +msgid "Turn On Microphone?" +msgstr "Zapnout mikrofon?" + +#: src/device.c:117 +msgid "" +"Access to your microphone can be changed at any time from the privacy " +"settings." +msgstr "" +"Přístup ke svému mikrofonu můžete kdykoliv změnit v nastavení soukromí." + +#: src/device.c:121 +msgid "An application wants to use your microphone." +msgstr "Nějaká aplikace chce používat váš mikrofon." + +#: src/device.c:123 +#, c-format +msgid "%s wants to use your microphone." +msgstr "Aplikace %s chce používat váš mikrofon." + +#: src/device.c:129 +msgid "Turn On Speakers?" +msgstr "Zapnout reproduktory?" + +#: src/device.c:130 +msgid "" +"Access to your speakers can be changed at any time from the privacy settings." +msgstr "" +"Přístup ke svým reproduktorům můžete kdykoliv změnit v nastavení soukromí." + +#: src/device.c:134 +msgid "An application wants to play sound." +msgstr "Nějaká aplikace chce přehrávat zvuk." + +#: src/device.c:136 +#, c-format +msgid "%s wants to play sound." +msgstr "Aplikace %s chce přehrávat zvuk." + +#: src/device.c:142 +msgid "Turn On Camera?" +msgstr "Zapnout kameru?" + +#: src/device.c:143 +msgid "" +"Access to your camera can be changed at any time from the privacy settings." +msgstr "Přístup ke své kameře můžete kdykoliv změnit v nastavení soukromí." + +#: src/device.c:147 +msgid "An application wants to use your camera." +msgstr "Nějaká aplikace chce používat vaši kameru." + +#: src/device.c:149 +#, c-format +msgid "%s wants to use your camera." +msgstr "Aplikace %s chce používat vaši kameru." + +#: src/location.c:526 +msgid "Deny Access" +msgstr "Zamítnout přístup" + +#: src/location.c:528 +msgid "Grant Access" +msgstr "Schválit přístup" + +#: src/location.c:534 +msgid "Grant Access to Your Location?" +msgstr "Schválit přístup k vaší poloze?" + +#: src/location.c:535 +msgid "An application wants to use your location." +msgstr "Nějaká aplikace chce použít vaši polohu." + +#: src/location.c:547 +#, c-format +msgid "Give %s Access to Your Location?" +msgstr "Schválit pro %s přístup k vaší poloze?" + +#: src/location.c:551 +#, c-format +msgid "%s wants to use your location." +msgstr "Aplikace %s chce použít vaši polohu." + +#: src/location.c:554 +msgid "Location access can be changed at any time from the privacy settings." +msgstr "" +"Přístup ke službám pro určování polohy můžete kdykoliv změnit v nastavení " +"soukromí." + +#: src/screenshot.c:236 src/wallpaper.c:180 +msgid "Deny" +msgstr "Zamítnout" + +#: src/screenshot.c:252 +#, c-format +msgid "Allow %s to Take Screenshots?" +msgstr "Povolit aplikaci %s pořizovat snímky obrazovky?" + +#: src/screenshot.c:253 +#, c-format +msgid "%s wants to be able to take screenshots at any time." +msgstr "Aplikace %s chce mít možnost kdykoliv pořizovat snímky obrazovky." + +#: src/screenshot.c:261 +msgid "Allow Applications to Take Screenshots?" +msgstr "Povolit aplikacím pořizovat snímky obrazovky?" + +#: src/screenshot.c:262 +msgid "An application wants to be able to take screenshots at any time." +msgstr "Nějaká aplikace chce mít možnost kdykoliv pořizovat snímky obrazovky." + +#: src/screenshot.c:265 src/wallpaper.c:209 +msgid "This permission can be changed at any time from the privacy settings." +msgstr "Toto oprávnění můžete kdykoliv změnit v nastavení soukromí." + +#: src/settings.c:127 +msgid "Requested setting not found" +msgstr "Požadované nastavení nebylo nalezeno" + +#: src/wallpaper.c:192 +msgid "Allow Applications to Set Backgrounds?" +msgstr "Povolit aplikacím možnost nastavit pozadí?" + +#: src/wallpaper.c:193 +msgid "An application is requesting to be able to change the background image." +msgstr "Aplikace požaduje mít možnost změnit obrázek na pozadí." + +#: src/wallpaper.c:205 +#, c-format +msgid "Allow %s to Set Backgrounds?" +msgstr "Povolit aplikaci %s možnost nastavit pozadí?" + +#: src/wallpaper.c:206 +#, c-format +msgid "%s is requesting to be able to change the background image." +msgstr "Aplikace %s požaduje mít možnost změnit obrázek na pozadí." diff --git a/po/da.po b/po/da.po new file mode 100644 index 0000000..8b5e8e6 --- /dev/null +++ b/po/da.po @@ -0,0 +1,189 @@ +# Danish translation for xdg-desktop-portal +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# scootergrisen, 2018-2020. +msgid "" +msgstr "" +"Project-Id-Version: xdg-desktop-portal\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-04 16:59-0300\n" +"PO-Revision-Date: 2020-10-12 01:25+0200\n" +"Last-Translator: scootergrisen\n" +"Language-Team: Danish\n" +"Language: da\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: src/background.c:741 +#, c-format +msgid "Allow %s to run in the background?" +msgstr "Tillad %s at køre i baggrunden?" + +#: src/background.c:745 +#, c-format +msgid "%s requests to be started automatically and run in the background." +msgstr "%s anmoder om at starte automatisk og køre i baggrunden." + +#: src/background.c:747 +#, c-format +msgid "%s requests to run in the background." +msgstr "%s anmoder om at køre i baggrunden." + +#: src/background.c:748 +msgid "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." +msgstr "" +"‘Kør i baggrunden’-tilladelsen kan når som helst ændres i " +"programindstillingerne." + +#: src/background.c:753 +msgid "Don't allow" +msgstr "Tillad ikke" + +#: src/background.c:754 src/screenshot.c:238 src/wallpaper.c:182 +msgid "Allow" +msgstr "Tillad" + +#: src/device.c:116 +msgid "Turn On Microphone?" +msgstr "Tænd for mikrofonen?" + +#: src/device.c:117 +msgid "" +"Access to your microphone can be changed at any time from the privacy " +"settings." +msgstr "" +"Adgang til din mikrofon kan når som helst ændres i privatlivsindstillingerne." + +#: src/device.c:121 +msgid "An application wants to use your microphone." +msgstr "Et program anmoder om at bruge din mikrofon." + +#: src/device.c:123 +#, c-format +msgid "%s wants to use your microphone." +msgstr "%s anmoder om at bruge din mikrofon." + +#: src/device.c:129 +msgid "Turn On Speakers?" +msgstr "Tænd for højttalerne?" + +#: src/device.c:130 +msgid "" +"Access to your speakers can be changed at any time from the privacy settings." +msgstr "" +"Adgang til dine højttalere kan når som helst ændres i " +"privatlivsindstillingerne." + +#: src/device.c:134 +msgid "An application wants to play sound." +msgstr "Et program anmoder om at afspille lyd." + +#: src/device.c:136 +#, c-format +msgid "%s wants to play sound." +msgstr "%s anmoder om at afspille lyd." + +#: src/device.c:142 +msgid "Turn On Camera?" +msgstr "Tænd for kameraet?" + +#: src/device.c:143 +msgid "" +"Access to your camera can be changed at any time from the privacy settings." +msgstr "" +"Adgang til dit kamera kan når som helst ændres i privatlivsindstillingerne." + +#: src/device.c:147 +msgid "An application wants to use your camera." +msgstr "Et program anmoder om at bruge dit kamera." + +#: src/device.c:149 +#, c-format +msgid "%s wants to use your camera." +msgstr "%s anmoder om at bruge dit kamera." + +#: src/location.c:526 +msgid "Deny Access" +msgstr "Nægt adgang" + +#: src/location.c:528 +msgid "Grant Access" +msgstr "Giv adgang" + +#: src/location.c:534 +msgid "Grant Access to Your Location?" +msgstr "Giv adgang til din placering?" + +#: src/location.c:535 +msgid "An application wants to use your location." +msgstr "Et program anmoder om at bruge din placering." + +#: src/location.c:547 +#, c-format +msgid "Give %s Access to Your Location?" +msgstr "Giv %s adgang til din placering?" + +#: src/location.c:551 +#, c-format +msgid "%s wants to use your location." +msgstr "%s anmoder om at bruge din placering." + +#: src/location.c:554 +msgid "Location access can be changed at any time from the privacy settings." +msgstr "" +"Adgang til din placering kan når som helst ændres i " +"privatlivsindstillingerne." + +#: src/screenshot.c:236 src/wallpaper.c:180 +msgid "Deny" +msgstr "Nægt" + +#: src/screenshot.c:252 +#, fuzzy, c-format +msgid "Allow %s to Take Screenshots?" +msgstr "Tillad %s at indstille baggrunde?" + +#: src/screenshot.c:253 +#, c-format +msgid "%s wants to be able to take screenshots at any time." +msgstr "" + +#: src/screenshot.c:261 +#, fuzzy +msgid "Allow Applications to Take Screenshots?" +msgstr "Tillad programmer at indstille baggrunde?" + +#: src/screenshot.c:262 +#, fuzzy +msgid "An application wants to be able to take screenshots at any time." +msgstr "Et program anmoder om at bruge din mikrofon." + +#: src/screenshot.c:265 src/wallpaper.c:209 +msgid "This permission can be changed at any time from the privacy settings." +msgstr "Tilladelsen kan når som helst ændres i privatlivsindstillingerne." + +#: src/settings.c:127 +msgid "Requested setting not found" +msgstr "Den anmodede indstilling blev ikke fundet" + +#: src/wallpaper.c:192 +msgid "Allow Applications to Set Backgrounds?" +msgstr "Tillad programmer at indstille baggrunde?" + +#: src/wallpaper.c:193 +msgid "An application is requesting to be able to change the background image." +msgstr "Et program anmoder om at være i stand til at ændre baggrundsbilledet." + +#: src/wallpaper.c:205 +#, c-format +msgid "Allow %s to Set Backgrounds?" +msgstr "Tillad %s at indstille baggrunde?" + +#: src/wallpaper.c:206 +#, c-format +msgid "%s is requesting to be able to change the background image." +msgstr "%s anmoder om at være i stand til at ændre baggrundsbilledet." diff --git a/po/de.po b/po/de.po new file mode 100644 index 0000000..f1053f2 --- /dev/null +++ b/po/de.po @@ -0,0 +1,201 @@ +# German translation for xdg-desktop-portal. +# Copyright (C) 2016 xdg-desktop-portal's COPYRIGHT HOLDER +# This file is distributed under the same license as the xdg-desktop-portal package. +# Benedikt M. Thoma , 2016. +# Mario Blättermann , 2016. +# +msgid "" +msgstr "" +"Project-Id-Version: xdg-desktop-portal master\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-04 16:59-0300\n" +"PO-Revision-Date: 2016-09-23 19:17+0200\n" +"Last-Translator: Mario Blättermann \n" +"Language-Team: German \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 1.8.9\n" + +#: src/background.c:741 +#, c-format +msgid "Allow %s to run in the background?" +msgstr "" + +#: src/background.c:745 +#, c-format +msgid "%s requests to be started automatically and run in the background." +msgstr "" + +#: src/background.c:747 +#, c-format +msgid "%s requests to run in the background." +msgstr "" + +#: src/background.c:748 +#, fuzzy +msgid "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." +msgstr "" +"Der Zugriff auf Ihre Lautsprecher kann in den Einstellungen zur Privatsphäre " +"jederzeit geändert werden." + +#: src/background.c:753 +msgid "Don't allow" +msgstr "" + +#: src/background.c:754 src/screenshot.c:238 src/wallpaper.c:182 +msgid "Allow" +msgstr "" + +#: src/device.c:116 +msgid "Turn On Microphone?" +msgstr "Mikrofon einschalten?" + +#: src/device.c:117 +msgid "" +"Access to your microphone can be changed at any time from the privacy " +"settings." +msgstr "" +"Der Zugriff auf Ihr Mikrofon kann in den Einstellungen zur Privatsphäre " +"jederzeit geändert werden." + +#: src/device.c:121 +msgid "An application wants to use your microphone." +msgstr "Eine Anwendung möchte auf Ihr Mikrofon zugreifen." + +#: src/device.c:123 +#, c-format +msgid "%s wants to use your microphone." +msgstr "%s möchte auf Ihr Mikrofon zugreifen." + +#: src/device.c:129 +msgid "Turn On Speakers?" +msgstr "Lautsprecher einschalten?" + +#: src/device.c:130 +msgid "" +"Access to your speakers can be changed at any time from the privacy settings." +msgstr "" +"Der Zugriff auf Ihre Lautsprecher kann in den Einstellungen zur Privatsphäre " +"jederzeit geändert werden." + +#: src/device.c:134 +msgid "An application wants to play sound." +msgstr "Eine Anwendung möchte Ton wiedergeben." + +#: src/device.c:136 +#, c-format +msgid "%s wants to play sound." +msgstr "%s möchte Ton wiedergeben." + +#: src/device.c:142 +msgid "Turn On Camera?" +msgstr "Kamera einschalten?" + +#: src/device.c:143 +msgid "" +"Access to your camera can be changed at any time from the privacy settings." +msgstr "" +"Der Zugriff auf Ihre Kamera kann in den Einstellungen zur Privatsphäre " +"jederzeit geändert werden." + +#: src/device.c:147 +msgid "An application wants to use your camera." +msgstr "Eine Anwendung möchte Ihre Kamera nutzen." + +#: src/device.c:149 +#, c-format +msgid "%s wants to use your camera." +msgstr "%s möchte Ihre Kamera nutzen." + +#: src/location.c:526 +msgid "Deny Access" +msgstr "" + +#: src/location.c:528 +msgid "Grant Access" +msgstr "" + +#: src/location.c:534 +msgid "Grant Access to Your Location?" +msgstr "" + +#: src/location.c:535 +#, fuzzy +msgid "An application wants to use your location." +msgstr "Eine Anwendung möchte Ihre Kamera nutzen." + +#: src/location.c:547 +#, c-format +msgid "Give %s Access to Your Location?" +msgstr "" + +#: src/location.c:551 +#, fuzzy, c-format +msgid "%s wants to use your location." +msgstr "%s möchte Ihre Kamera nutzen." + +#: src/location.c:554 +#, fuzzy +msgid "Location access can be changed at any time from the privacy settings." +msgstr "" +"Der Zugriff auf Ihre Lautsprecher kann in den Einstellungen zur Privatsphäre " +"jederzeit geändert werden." + +#: src/screenshot.c:236 src/wallpaper.c:180 +msgid "Deny" +msgstr "" + +#: src/screenshot.c:252 +#, c-format +msgid "Allow %s to Take Screenshots?" +msgstr "" + +#: src/screenshot.c:253 +#, c-format +msgid "%s wants to be able to take screenshots at any time." +msgstr "" + +#: src/screenshot.c:261 +#, fuzzy +msgid "Allow Applications to Take Screenshots?" +msgstr "Eine Anwendung möchte Ton wiedergeben." + +#: src/screenshot.c:262 +#, fuzzy +msgid "An application wants to be able to take screenshots at any time." +msgstr "Eine Anwendung möchte auf Ihr Mikrofon zugreifen." + +#: src/screenshot.c:265 src/wallpaper.c:209 +#, fuzzy +msgid "This permission can be changed at any time from the privacy settings." +msgstr "" +"Der Zugriff auf Ihre Lautsprecher kann in den Einstellungen zur Privatsphäre " +"jederzeit geändert werden." + +#: src/settings.c:127 +msgid "Requested setting not found" +msgstr "" + +#: src/wallpaper.c:192 +#, fuzzy +msgid "Allow Applications to Set Backgrounds?" +msgstr "Eine Anwendung möchte Ton wiedergeben." + +#: src/wallpaper.c:193 +msgid "An application is requesting to be able to change the background image." +msgstr "" + +#: src/wallpaper.c:205 +#, c-format +msgid "Allow %s to Set Backgrounds?" +msgstr "" + +#: src/wallpaper.c:206 +#, c-format +msgid "%s is requesting to be able to change the background image." +msgstr "" diff --git a/po/en_GB.po b/po/en_GB.po new file mode 100644 index 0000000..d722568 --- /dev/null +++ b/po/en_GB.po @@ -0,0 +1,191 @@ +# British English translation for xdg-desktop-portal. +# Copyright (C) 2019 xdg-desktop-portal's COPYRIGHT HOLDER +# This file is distributed under the same license as the xdg-desktop-portal package. +# Zander Brown , 2019. +# +msgid "" +msgstr "" +"Project-Id-Version: xdg-desktop-portal master\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-04 16:59-0300\n" +"PO-Revision-Date: 2019-08-24 23:48+0100\n" +"Last-Translator: Zander Brown \n" +"Language-Team: English - United Kingdom \n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Gtranslator 3.32.1\n" + +#: src/background.c:741 +#, c-format +msgid "Allow %s to run in the background?" +msgstr "Allow %s to run in the background?" + +#: src/background.c:745 +#, c-format +msgid "%s requests to be started automatically and run in the background." +msgstr "%s requests to be started automatically and run in the background." + +#: src/background.c:747 +#, c-format +msgid "%s requests to run in the background." +msgstr "%s requests to run in the background." + +#: src/background.c:748 +msgid "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." +msgstr "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." + +#: src/background.c:753 +msgid "Don't allow" +msgstr "Don't allow" + +#: src/background.c:754 src/screenshot.c:238 src/wallpaper.c:182 +msgid "Allow" +msgstr "Allow" + +#: src/device.c:116 +msgid "Turn On Microphone?" +msgstr "Turn On Microphone?" + +#: src/device.c:117 +msgid "" +"Access to your microphone can be changed at any time from the privacy " +"settings." +msgstr "" +"Access to your microphone can be changed at any time from the privacy " +"settings." + +#: src/device.c:121 +msgid "An application wants to use your microphone." +msgstr "An application wants to use your microphone." + +#: src/device.c:123 +#, c-format +msgid "%s wants to use your microphone." +msgstr "%s wants to use your microphone." + +#: src/device.c:129 +msgid "Turn On Speakers?" +msgstr "Turn On Speakers?" + +#: src/device.c:130 +msgid "" +"Access to your speakers can be changed at any time from the privacy settings." +msgstr "" +"Access to your speakers can be changed at any time from the privacy settings." + +#: src/device.c:134 +msgid "An application wants to play sound." +msgstr "An application wants to play sound." + +#: src/device.c:136 +#, c-format +msgid "%s wants to play sound." +msgstr "%s wants to play sound." + +#: src/device.c:142 +msgid "Turn On Camera?" +msgstr "Turn On Camera?" + +#: src/device.c:143 +msgid "" +"Access to your camera can be changed at any time from the privacy settings." +msgstr "" +"Access to your camera can be changed at any time from the privacy settings." + +#: src/device.c:147 +msgid "An application wants to use your camera." +msgstr "An application wants to use your camera." + +#: src/device.c:149 +#, c-format +msgid "%s wants to use your camera." +msgstr "%s wants to use your camera." + +#: src/location.c:526 +msgid "Deny Access" +msgstr "Deny Access" + +#: src/location.c:528 +msgid "Grant Access" +msgstr "Grant Access" + +#: src/location.c:534 +msgid "Grant Access to Your Location?" +msgstr "Grant Access to Your Location?" + +#: src/location.c:535 +msgid "An application wants to use your location." +msgstr "An application wants to use your location." + +#: src/location.c:547 +#, c-format +msgid "Give %s Access to Your Location?" +msgstr "Give %s Access to Your Location?" + +#: src/location.c:551 +#, c-format +msgid "%s wants to use your location." +msgstr "%s wants to use your location." + +#: src/location.c:554 +msgid "Location access can be changed at any time from the privacy settings." +msgstr "Location access can be changed at any time from the privacy settings." + +#: src/screenshot.c:236 src/wallpaper.c:180 +msgid "Deny" +msgstr "" + +#: src/screenshot.c:252 +#, fuzzy, c-format +msgid "Allow %s to Take Screenshots?" +msgstr "Allow %s to run in the background?" + +#: src/screenshot.c:253 +#, c-format +msgid "%s wants to be able to take screenshots at any time." +msgstr "" + +#: src/screenshot.c:261 +#, fuzzy +msgid "Allow Applications to Take Screenshots?" +msgstr "Allow %s to run in the background?" + +#: src/screenshot.c:262 +#, fuzzy +msgid "An application wants to be able to take screenshots at any time." +msgstr "An application wants to use your microphone." + +#: src/screenshot.c:265 src/wallpaper.c:209 +#, fuzzy +msgid "This permission can be changed at any time from the privacy settings." +msgstr "Location access can be changed at any time from the privacy settings." + +#: src/settings.c:127 +msgid "Requested setting not found" +msgstr "Requested setting not found" + +#: src/wallpaper.c:192 +#, fuzzy +msgid "Allow Applications to Set Backgrounds?" +msgstr "Allow %s to run in the background?" + +#: src/wallpaper.c:193 +msgid "An application is requesting to be able to change the background image." +msgstr "" + +#: src/wallpaper.c:205 +#, fuzzy, c-format +msgid "Allow %s to Set Backgrounds?" +msgstr "Allow %s to run in the background?" + +#: src/wallpaper.c:206 +#, fuzzy, c-format +msgid "%s is requesting to be able to change the background image." +msgstr "%s requests to run in the background." diff --git a/po/es.po b/po/es.po new file mode 100644 index 0000000..7c61806 --- /dev/null +++ b/po/es.po @@ -0,0 +1,194 @@ +# Spanish translation for xdg-desktop-portal. +# Copyright (C) 2018 xdg-desktop-portal's COPYRIGHT HOLDER +# This file is distributed under the same license as the xdg-desktop-portal package. +# Adolfo Jayme Barrientos , 2018. +msgid "" +msgstr "" +"Project-Id-Version: xdg-desktop-portal master\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-04 16:59-0300\n" +"PO-Revision-Date: 2021-12-04 14:52+0100\n" +"Last-Translator: Adolfo Jayme Barrientos \n" +"Language-Team: Spanish\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.0\n" + +#: src/background.c:741 +#, c-format +msgid "Allow %s to run in the background?" +msgstr "¿Quiere permitir que %s se ejecute en segundo plano?" + +#: src/background.c:745 +#, c-format +msgid "%s requests to be started automatically and run in the background." +msgstr "%s solicita iniciarse automáticamente y ejecutarse en segundo plano." + +#: src/background.c:747 +#, c-format +msgid "%s requests to run in the background." +msgstr "%s solicita ejecutarse en segundo plano." + +#: src/background.c:748 +msgid "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." +msgstr "" +"El permiso «ejecución en segundo plano» puede modificarse en cualquier " +"momento a través de la configuración de privacidad." + +#: src/background.c:753 +msgid "Don't allow" +msgstr "No permitir" + +#: src/background.c:754 src/screenshot.c:238 src/wallpaper.c:182 +msgid "Allow" +msgstr "Permitir" + +#: src/device.c:116 +msgid "Turn On Microphone?" +msgstr "¿Quiere encender el micrófono?" + +#: src/device.c:117 +msgid "" +"Access to your microphone can be changed at any time from the privacy " +"settings." +msgstr "" +"El acceso al micrófono puede modificarse en cualquier momento a través de la " +"configuración de privacidad." + +#: src/device.c:121 +msgid "An application wants to use your microphone." +msgstr "Una aplicación intenta utilizar el micrófono." + +#: src/device.c:123 +#, c-format +msgid "%s wants to use your microphone." +msgstr "%s intenta utilizar el micrófono." + +#: src/device.c:129 +msgid "Turn On Speakers?" +msgstr "¿Quiere encender los altavoces?" + +#: src/device.c:130 +msgid "" +"Access to your speakers can be changed at any time from the privacy settings." +msgstr "" +"El acceso a los altavoces puede modificarse en cualquier momento a través de " +"la configuración de privacidad." + +#: src/device.c:134 +msgid "An application wants to play sound." +msgstr "Una aplicación intenta reproducir sonido." + +#: src/device.c:136 +#, c-format +msgid "%s wants to play sound." +msgstr "%s intenta reproducir sonido." + +#: src/device.c:142 +msgid "Turn On Camera?" +msgstr "¿Quiere encender la cámara?" + +#: src/device.c:143 +msgid "" +"Access to your camera can be changed at any time from the privacy settings." +msgstr "" +"El acceso a la cámara puede modificarse en cualquier momento a través de la " +"configuración de privacidad." + +#: src/device.c:147 +msgid "An application wants to use your camera." +msgstr "Una aplicación intenta utilizar la cámara." + +#: src/device.c:149 +#, c-format +msgid "%s wants to use your camera." +msgstr "%s intenta utilizar la cámara." + +#: src/location.c:526 +msgid "Deny Access" +msgstr "Denegar acceso" + +#: src/location.c:528 +msgid "Grant Access" +msgstr "Conceder acceso" + +#: src/location.c:534 +msgid "Grant Access to Your Location?" +msgstr "¿Quiere conceder acceso a su ubicación?" + +#: src/location.c:535 +msgid "An application wants to use your location." +msgstr "Una aplicación intenta utilizar su ubicación." + +#: src/location.c:547 +#, c-format +msgid "Give %s Access to Your Location?" +msgstr "¿Quiere conceder a %s acceso a su ubicación?" + +#: src/location.c:551 +#, c-format +msgid "%s wants to use your location." +msgstr "%s intenta utilizar su ubicación." + +#: src/location.c:554 +msgid "Location access can be changed at any time from the privacy settings." +msgstr "" +"El acceso a la ubicación puede modificarse en cualquier momento a través de " +"la configuración de privacidad." + +#: src/screenshot.c:236 src/wallpaper.c:180 +msgid "Deny" +msgstr "Denegar" + +#: src/screenshot.c:252 +#, fuzzy, c-format +msgid "Allow %s to Take Screenshots?" +msgstr "¿Quiere permitir que %s establezca fondos?" + +#: src/screenshot.c:253 +#, c-format +msgid "%s wants to be able to take screenshots at any time." +msgstr "" + +#: src/screenshot.c:261 +#, fuzzy +msgid "Allow Applications to Take Screenshots?" +msgstr "¿Quiere permitir que las aplicaciones establezcan fondos?" + +#: src/screenshot.c:262 +#, fuzzy +msgid "An application wants to be able to take screenshots at any time." +msgstr "Una aplicación intenta utilizar el micrófono." + +#: src/screenshot.c:265 src/wallpaper.c:209 +msgid "This permission can be changed at any time from the privacy settings." +msgstr "" +"Este permiso puede modificarse en cualquier momento a través de la " +"configuración de privacidad." + +#: src/settings.c:127 +msgid "Requested setting not found" +msgstr "No se encontró la configuración solicitada" + +#: src/wallpaper.c:192 +msgid "Allow Applications to Set Backgrounds?" +msgstr "¿Quiere permitir que las aplicaciones establezcan fondos?" + +#: src/wallpaper.c:193 +msgid "An application is requesting to be able to change the background image." +msgstr "Una aplicación solicita poder cambiar la imagen de fondo." + +#: src/wallpaper.c:205 +#, c-format +msgid "Allow %s to Set Backgrounds?" +msgstr "¿Quiere permitir que %s establezca fondos?" + +#: src/wallpaper.c:206 +#, c-format +msgid "%s is requesting to be able to change the background image." +msgstr "%s solicita poder cambiar la imagen de fondo." diff --git a/po/fr.po b/po/fr.po new file mode 100644 index 0000000..198ccef --- /dev/null +++ b/po/fr.po @@ -0,0 +1,199 @@ +# French translation for xdg-desktop-portal. +# Copyright (C) 2016 xdg-desktop-portal's COPYRIGHT HOLDER +# This file is distributed under the same license as the xdg-desktop-portal package. +# Nicolas Cuffia , 2016. +# +msgid "" +msgstr "" +"Project-Id-Version: xdg-desktop-portal master\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-04 16:59-0300\n" +"PO-Revision-Date: 2017-03-02 11:34+0100\n" +"Last-Translator: Nicolas Cuffia \n" +"Language-Team: French \n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: src/background.c:741 +#, c-format +msgid "Allow %s to run in the background?" +msgstr "" + +#: src/background.c:745 +#, c-format +msgid "%s requests to be started automatically and run in the background." +msgstr "" + +#: src/background.c:747 +#, c-format +msgid "%s requests to run in the background." +msgstr "" + +#: src/background.c:748 +#, fuzzy +msgid "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." +msgstr "" +"L'accès aux haut-parleurs peut être modifié n'importe quand à partir des " +"paramètres de confidentialité." + +#: src/background.c:753 +msgid "Don't allow" +msgstr "" + +#: src/background.c:754 src/screenshot.c:238 src/wallpaper.c:182 +msgid "Allow" +msgstr "" + +#: src/device.c:116 +msgid "Turn On Microphone?" +msgstr "Allumer le microphone ?" + +#: src/device.c:117 +msgid "" +"Access to your microphone can be changed at any time from the privacy " +"settings." +msgstr "" +"L'accès au microphone peut être modifié n'importe quand à partir des " +"paramètres de confidentialité." + +#: src/device.c:121 +msgid "An application wants to use your microphone." +msgstr "Une application veut utiliser votre microphone." + +#: src/device.c:123 +#, c-format +msgid "%s wants to use your microphone." +msgstr "%s veut utiliser votre microphone." + +#: src/device.c:129 +msgid "Turn On Speakers?" +msgstr "Allumer les haut-parleurs ?" + +#: src/device.c:130 +msgid "" +"Access to your speakers can be changed at any time from the privacy settings." +msgstr "" +"L'accès aux haut-parleurs peut être modifié n'importe quand à partir des " +"paramètres de confidentialité." + +#: src/device.c:134 +msgid "An application wants to play sound." +msgstr "Une application veut jouer du son." + +#: src/device.c:136 +#, c-format +msgid "%s wants to play sound." +msgstr "%s veut jouer du son." + +#: src/device.c:142 +msgid "Turn On Camera?" +msgstr "Allumer la caméra ?" + +#: src/device.c:143 +msgid "" +"Access to your camera can be changed at any time from the privacy settings." +msgstr "" +"L'accès à la caméra peut être modifié n'importe quand à partir des " +"paramètres de confidentialité." + +#: src/device.c:147 +msgid "An application wants to use your camera." +msgstr "Une application veut utiliser votre caméra." + +#: src/device.c:149 +#, c-format +msgid "%s wants to use your camera." +msgstr "%s veut utiliser votre caméra." + +#: src/location.c:526 +msgid "Deny Access" +msgstr "" + +#: src/location.c:528 +msgid "Grant Access" +msgstr "" + +#: src/location.c:534 +msgid "Grant Access to Your Location?" +msgstr "" + +#: src/location.c:535 +#, fuzzy +msgid "An application wants to use your location." +msgstr "Une application veut utiliser votre caméra." + +#: src/location.c:547 +#, c-format +msgid "Give %s Access to Your Location?" +msgstr "" + +#: src/location.c:551 +#, fuzzy, c-format +msgid "%s wants to use your location." +msgstr "%s veut utiliser votre caméra." + +#: src/location.c:554 +#, fuzzy +msgid "Location access can be changed at any time from the privacy settings." +msgstr "" +"L'accès aux haut-parleurs peut être modifié n'importe quand à partir des " +"paramètres de confidentialité." + +#: src/screenshot.c:236 src/wallpaper.c:180 +msgid "Deny" +msgstr "" + +#: src/screenshot.c:252 +#, c-format +msgid "Allow %s to Take Screenshots?" +msgstr "" + +#: src/screenshot.c:253 +#, c-format +msgid "%s wants to be able to take screenshots at any time." +msgstr "" + +#: src/screenshot.c:261 +#, fuzzy +msgid "Allow Applications to Take Screenshots?" +msgstr "Une application veut jouer du son." + +#: src/screenshot.c:262 +#, fuzzy +msgid "An application wants to be able to take screenshots at any time." +msgstr "Une application veut utiliser votre microphone." + +#: src/screenshot.c:265 src/wallpaper.c:209 +#, fuzzy +msgid "This permission can be changed at any time from the privacy settings." +msgstr "" +"L'accès aux haut-parleurs peut être modifié n'importe quand à partir des " +"paramètres de confidentialité." + +#: src/settings.c:127 +msgid "Requested setting not found" +msgstr "" + +#: src/wallpaper.c:192 +#, fuzzy +msgid "Allow Applications to Set Backgrounds?" +msgstr "Une application veut jouer du son." + +#: src/wallpaper.c:193 +msgid "An application is requesting to be able to change the background image." +msgstr "" + +#: src/wallpaper.c:205 +#, c-format +msgid "Allow %s to Set Backgrounds?" +msgstr "" + +#: src/wallpaper.c:206 +#, c-format +msgid "%s is requesting to be able to change the background image." +msgstr "" diff --git a/po/gl.po b/po/gl.po new file mode 100644 index 0000000..8393597 --- /dev/null +++ b/po/gl.po @@ -0,0 +1,194 @@ +# Galician translation for xdg-desktop-portal. +# Copyright (C) 2017 xdg-desktop-portal's COPYRIGHT HOLDER +# This file is distributed under the same license as the xdg-desktop-portal package. +# Fran Dieguez , 2017, 2019. +msgid "" +msgstr "" +"Project-Id-Version: xdg-desktop-portal master\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-04 16:59-0300\n" +"PO-Revision-Date: 2023-02-08 21:13+0100\n" +"Last-Translator: Fran Diéguez \n" +"Language-Team: Galician\n" +"Language: gl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.2.2\n" + +#: src/background.c:741 +#, c-format +msgid "Allow %s to run in the background?" +msgstr "Permitir a %s executarse en segundo plano?" + +#: src/background.c:745 +#, c-format +msgid "%s requests to be started automatically and run in the background." +msgstr "%s solicita iniciarse automaticamente e en segundo plano." + +#: src/background.c:747 +#, c-format +msgid "%s requests to run in the background." +msgstr "%s solicita executarse en segundo plano." + +#: src/background.c:748 +msgid "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." +msgstr "" +"Pode cambiar o permiso «executarse en segundo plano» en calquera momento " +"desde as preferencias da aplicación." + +#: src/background.c:753 +msgid "Don't allow" +msgstr "Non permitir" + +#: src/background.c:754 src/screenshot.c:238 src/wallpaper.c:182 +msgid "Allow" +msgstr "Permitir" + +#: src/device.c:116 +msgid "Turn On Microphone?" +msgstr "Acender micrófono?" + +#: src/device.c:117 +msgid "" +"Access to your microphone can be changed at any time from the privacy " +"settings." +msgstr "" +"O acceso ao seu micrófono pode cambiarse en calquera momento desde as " +"preferencias de privacidade." + +#: src/device.c:121 +msgid "An application wants to use your microphone." +msgstr "Unha aplicación quere usar o seu micrófono." + +#: src/device.c:123 +#, c-format +msgid "%s wants to use your microphone." +msgstr "%s quere uar o seu micrófono." + +#: src/device.c:129 +msgid "Turn On Speakers?" +msgstr "Acender os altofalantes?" + +#: src/device.c:130 +msgid "" +"Access to your speakers can be changed at any time from the privacy settings." +msgstr "" +"O acceso aos seus altofalantes pode cambiarse en calquera momento desde as " +"preferencias de privacidade." + +#: src/device.c:134 +msgid "An application wants to play sound." +msgstr "Unha aplicación quere reproducir son." + +#: src/device.c:136 +#, c-format +msgid "%s wants to play sound." +msgstr "%s querer reproducir son." + +#: src/device.c:142 +msgid "Turn On Camera?" +msgstr "Acender a cámara?" + +#: src/device.c:143 +msgid "" +"Access to your camera can be changed at any time from the privacy settings." +msgstr "" +"O acceso á súa cámara pode cambiarse en calquera momento desde as " +"preferencias de privacidade." + +#: src/device.c:147 +msgid "An application wants to use your camera." +msgstr "Unha aplicación quere usar a súa cámara." + +#: src/device.c:149 +#, c-format +msgid "%s wants to use your camera." +msgstr "%s quere uar a súa cámara." + +#: src/location.c:526 +msgid "Deny Access" +msgstr "Denegar acceso" + +#: src/location.c:528 +msgid "Grant Access" +msgstr "Conceder acceso" + +#: src/location.c:534 +msgid "Grant Access to Your Location?" +msgstr "Desexa acceder acceso á súa localización?" + +#: src/location.c:535 +msgid "An application wants to use your location." +msgstr "Unha aplicación quere usar a súa localización." + +#: src/location.c:547 +#, c-format +msgid "Give %s Access to Your Location?" +msgstr "Desexa darlle acceso a %s á súa localización?" + +#: src/location.c:551 +#, c-format +msgid "%s wants to use your location." +msgstr "%s quere usar a súa localización." + +#: src/location.c:554 +msgid "Location access can be changed at any time from the privacy settings." +msgstr "" +"O acceso á súa localización pode cambiarse en calquera momento desde as " +"preferencias de privacidade." + +#: src/screenshot.c:236 src/wallpaper.c:180 +msgid "Deny" +msgstr "Denegar" + +#: src/screenshot.c:252 +#, c-format +msgid "Allow %s to Take Screenshots?" +msgstr "Permitirlle a %s configurar sacar capturas de pantalla?" + +#: src/screenshot.c:253 +#, c-format +msgid "%s wants to be able to take screenshots at any time." +msgstr "%s solicita poder sacar capturas de pantalla en calquer momento." + +#: src/screenshot.c:261 +msgid "Allow Applications to Take Screenshots?" +msgstr "Permitirlle ás aplicacións sacar capturas de pantalla?" + +#: src/screenshot.c:262 +msgid "An application wants to be able to take screenshots at any time." +msgstr "" +"Unha aplicación solicita poder sacar capturas de pantalla en calquera " +"momento." + +#: src/screenshot.c:265 src/wallpaper.c:209 +msgid "This permission can be changed at any time from the privacy settings." +msgstr "" +"Este permiso pode cambiarse en calquera momento desde as preferencias de " +"privacidade." + +#: src/settings.c:127 +msgid "Requested setting not found" +msgstr "Non se atopou a configuración solicitada" + +#: src/wallpaper.c:192 +msgid "Allow Applications to Set Backgrounds?" +msgstr "Permitirlle ás aplicacións configurar o fondo de pantalla?" + +#: src/wallpaper.c:193 +msgid "An application is requesting to be able to change the background image." +msgstr "Unha aplicación está solicitando poder cambiar a imaxe de fondo." + +#: src/wallpaper.c:205 +#, c-format +msgid "Allow %s to Set Backgrounds?" +msgstr "Permitirlle a %s configurar o fondo de pantalla?" + +#: src/wallpaper.c:206 +#, c-format +msgid "%s is requesting to be able to change the background image." +msgstr "%s está solicitando poder cambiar o fondo de pantalla." diff --git a/po/he.po b/po/he.po new file mode 100644 index 0000000..2f03026 --- /dev/null +++ b/po/he.po @@ -0,0 +1,183 @@ +# Hebrew translation for xdg-desktop-portal. +# Copyright (C) 2022 xdg-desktop-portal's COPYRIGHT HOLDER +# This file is distributed under the same license as the xdg-desktop-portal package. +# Yosef Or Boczko , 2022. +# +msgid "" +msgstr "" +"Project-Id-Version: xdg-desktop-portal main\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-04 16:59-0300\n" +"PO-Revision-Date: 2022-07-25 21:56+0300\n" +"Last-Translator: Yosef Or Boczko \n" +"Language-Team: Hebrew \n" +"Language: he\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"X-Generator: Gtranslator 40.0\n" + +#: src/background.c:741 +#, c-format +msgid "Allow %s to run in the background?" +msgstr "לאפשר הרצה של %s ברקע?" + +#: src/background.c:745 +#, c-format +msgid "%s requests to be started automatically and run in the background." +msgstr "‏%s מבקש להיפתח באופן אוטומטי ולרוץ ברקע." + +#: src/background.c:747 +#, c-format +msgid "%s requests to run in the background." +msgstr "‏%s מבקש לרוץ ברקע." + +#: src/background.c:748 +msgid "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." +msgstr "ניתן לשנות בכל עת את ההרשאה „לפעול ברקע” בהגדרות היישום." + +#: src/background.c:753 +msgid "Don't allow" +msgstr "לא לאפשר" + +#: src/background.c:754 src/screenshot.c:238 src/wallpaper.c:182 +msgid "Allow" +msgstr "לאפשר" + +#: src/device.c:116 +msgid "Turn On Microphone?" +msgstr "להפעיל את המיקרופון?" + +#: src/device.c:117 +msgid "" +"Access to your microphone can be changed at any time from the privacy " +"settings." +msgstr "ניתן לשנות בכל עת את הרשאות הגישה למיקרופון שלך בהגדרות הפרטיות." + +#: src/device.c:121 +msgid "An application wants to use your microphone." +msgstr "יישום מבקש להשתמש במיקרופון שלך." + +#: src/device.c:123 +#, c-format +msgid "%s wants to use your microphone." +msgstr "‏%s מבקש להשתמש במיקרופון שלך." + +#: src/device.c:129 +msgid "Turn On Speakers?" +msgstr "להפעיל את הרמקולים?" + +#: src/device.c:130 +msgid "" +"Access to your speakers can be changed at any time from the privacy settings." +msgstr "ניתן לשנות בכל עת את הרשאות הגישה לרמקולים שלך בהגדרות הפרטיות." + +#: src/device.c:134 +msgid "An application wants to play sound." +msgstr "יישום מבקש להפעיל שמע." + +#: src/device.c:136 +#, c-format +msgid "%s wants to play sound." +msgstr "‏%s מבקש להפעיל שמע." + +#: src/device.c:142 +msgid "Turn On Camera?" +msgstr "להפעיל את המצלמה?" + +#: src/device.c:143 +msgid "" +"Access to your camera can be changed at any time from the privacy settings." +msgstr "ניתן לשנות בכל עת את הרשאות הגישה למצלמה שלך בהגדרות הפרטיות." + +#: src/device.c:147 +msgid "An application wants to use your camera." +msgstr "יישום מבקש להשתמש במצלמה שלך." + +#: src/device.c:149 +#, c-format +msgid "%s wants to use your camera." +msgstr "‏%s מבקש להשתמש במצלמה שלך." + +#: src/location.c:526 +msgid "Deny Access" +msgstr "מניעת גישה" + +#: src/location.c:528 +msgid "Grant Access" +msgstr "הענקת גישה" + +#: src/location.c:534 +msgid "Grant Access to Your Location?" +msgstr "לאפשר גישה למיקום שלך?" + +#: src/location.c:535 +msgid "An application wants to use your location." +msgstr "יישום מבקש להשתמש במיקום שלך." + +#: src/location.c:547 +#, c-format +msgid "Give %s Access to Your Location?" +msgstr "לתת ליישום %s גישה למיקום שלך?" + +#: src/location.c:551 +#, c-format +msgid "%s wants to use your location." +msgstr "‏%s מבקש להשתמש במיקום שלך." + +#: src/location.c:554 +msgid "Location access can be changed at any time from the privacy settings." +msgstr "ניתן לשנות בכל עת את הרשאות הגישה למיקום שלך מהגדרות הפרטיות." + +#: src/screenshot.c:236 src/wallpaper.c:180 +msgid "Deny" +msgstr "דחייה" + +#: src/screenshot.c:252 +#, fuzzy, c-format +msgid "Allow %s to Take Screenshots?" +msgstr "לאפשר ליישום %s לקבוע את הרקעים?" + +#: src/screenshot.c:253 +#, c-format +msgid "%s wants to be able to take screenshots at any time." +msgstr "" + +#: src/screenshot.c:261 +#, fuzzy +msgid "Allow Applications to Take Screenshots?" +msgstr "לאפשר ליישומים לקבוע את הרקעים?" + +#: src/screenshot.c:262 +#, fuzzy +msgid "An application wants to be able to take screenshots at any time." +msgstr "יישום מבקש להשתמש במיקרופון שלך." + +#: src/screenshot.c:265 src/wallpaper.c:209 +msgid "This permission can be changed at any time from the privacy settings." +msgstr "ניתן לשנות בכל עת הרשאה זו בהגדרות הפרטיות." + +#: src/settings.c:127 +msgid "Requested setting not found" +msgstr "ההגדרה המבוקשת לא נמצאה" + +#: src/wallpaper.c:192 +msgid "Allow Applications to Set Backgrounds?" +msgstr "לאפשר ליישומים לקבוע את הרקעים?" + +#: src/wallpaper.c:193 +msgid "An application is requesting to be able to change the background image." +msgstr "יישום מבקש הרשאה לשינוי תמונת הרקע." + +#: src/wallpaper.c:205 +#, c-format +msgid "Allow %s to Set Backgrounds?" +msgstr "לאפשר ליישום %s לקבוע את הרקעים?" + +#: src/wallpaper.c:206 +#, c-format +msgid "%s is requesting to be able to change the background image." +msgstr "‏%s מבקש הרשאה לשינוי תמונת הרקע." diff --git a/po/hi.po b/po/hi.po new file mode 100644 index 0000000..9a387f9 --- /dev/null +++ b/po/hi.po @@ -0,0 +1,183 @@ +# Hindi translation for xdg-desktop-portal. +# Copyright (C) 2021 xdg-desktop-portal's COPYRIGHT HOLDER +# This file is distributed under the same license as the xdg-desktop-portal package. +# Dmitry , 2021. +# +msgid "" +msgstr "" +"Project-Id-Version: xdg-desktop-portal 1.8.1\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-04 16:59-0300\n" +"PO-Revision-Date: 2021-09-09 00:42+0700\n" +"Last-Translator: Dmitry \n" +"Language-Team: Hindi \n" +"Language: hi\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 2.2.4\n" +"Plural-Forms: nplurals=2; plural=(n==0 || n==1);\n" + +#: src/background.c:741 +#, c-format +msgid "Allow %s to run in the background?" +msgstr "अनुमति देना %s पृष्ठभूमि में चलाने के लिए?" + +#: src/background.c:745 +#, c-format +msgid "%s requests to be started automatically and run in the background." +msgstr "%s अनुरोध स्वचालित रूप से शुरू होने और पृष्ठभूमि में चलाने का अनुरोध करता है।" + +#: src/background.c:747 +#, c-format +msgid "%s requests to run in the background." +msgstr "%s पृष्ठभूमि में चलाने के लिए अनुरोध।" + +#: src/background.c:748 +msgid "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." +msgstr "एप्लिकेशन सेटिंग से किसी भी समय 'रन इन बैकग्राउंड' अनुमति को बदला जा सकता है।" + +#: src/background.c:753 +msgid "Don't allow" +msgstr "अनुमति न दें" + +#: src/background.c:754 src/screenshot.c:238 src/wallpaper.c:182 +msgid "Allow" +msgstr "अनुमति" + +#: src/device.c:116 +msgid "Turn On Microphone?" +msgstr "माइक्रोफ़ोन चालू करें?" + +#: src/device.c:117 +msgid "" +"Access to your microphone can be changed at any time from the privacy " +"settings." +msgstr "गोपनीयता सेटिंग्स से आपके माइक्रोफ़ोन तक पहुंच को किसी भी समय बदला जा सकता है।" + +#: src/device.c:121 +msgid "An application wants to use your microphone." +msgstr "एक एप्लिकेशन आपके माइक्रोफ़ोन का उपयोग करना चाहता है।" + +#: src/device.c:123 +#, c-format +msgid "%s wants to use your microphone." +msgstr "%s आपके माइक्रोफ़ोन का उपयोग करना चाहता है।" + +#: src/device.c:129 +msgid "Turn On Speakers?" +msgstr "स्पीकर चालू करें?" + +#: src/device.c:130 +msgid "" +"Access to your speakers can be changed at any time from the privacy settings." +msgstr "गोपनीयता सेटिंग्स से आपके स्पीकर तक पहुंच को किसी भी समय बदला जा सकता है।" + +#: src/device.c:134 +msgid "An application wants to play sound." +msgstr "एक एप्लिकेशन ध्वनि बजाना चाहता है।" + +#: src/device.c:136 +#, c-format +msgid "%s wants to play sound." +msgstr "%s ध्वनि बजाना चाहता है।" + +#: src/device.c:142 +msgid "Turn On Camera?" +msgstr "कैमरा चालू करें?" + +#: src/device.c:143 +msgid "" +"Access to your camera can be changed at any time from the privacy settings." +msgstr "गोपनीयता सेटिंग्स से आपके कैमरे तक पहुंच को किसी भी समय बदला जा सकता है।" + +#: src/device.c:147 +msgid "An application wants to use your camera." +msgstr "एक एप्लिकेशन आपके कैमरे का उपयोग करना चाहता है।" + +#: src/device.c:149 +#, c-format +msgid "%s wants to use your camera." +msgstr "%s अपने कैमरे का उपयोग करना चाहता है।" + +#: src/location.c:526 +msgid "Deny Access" +msgstr "पहुँच को अस्वीकार" + +#: src/location.c:528 +msgid "Grant Access" +msgstr "अनुदान पहुँच" + +#: src/location.c:534 +msgid "Grant Access to Your Location?" +msgstr "अपने स्थान तक पहुंच प्रदान करें?" + +#: src/location.c:535 +msgid "An application wants to use your location." +msgstr "एक एप्लिकेशन आपके स्थान का उपयोग करना चाहता है।" + +#: src/location.c:547 +#, c-format +msgid "Give %s Access to Your Location?" +msgstr "देना %s आपके स्थान तक पहुँच?" + +#: src/location.c:551 +#, c-format +msgid "%s wants to use your location." +msgstr "%s आपके स्थान का उपयोग करना चाहता है।" + +#: src/location.c:554 +msgid "Location access can be changed at any time from the privacy settings." +msgstr "गोपनीयता सेटिंग्स से किसी भी समय स्थान का उपयोग बदला जा सकता है।" + +#: src/screenshot.c:236 src/wallpaper.c:180 +msgid "Deny" +msgstr "मना" + +#: src/screenshot.c:252 +#, fuzzy, c-format +msgid "Allow %s to Take Screenshots?" +msgstr "परमिट %s पृष्ठभूमि सेट करने के लिए?" + +#: src/screenshot.c:253 +#, c-format +msgid "%s wants to be able to take screenshots at any time." +msgstr "" + +#: src/screenshot.c:261 +#, fuzzy +msgid "Allow Applications to Take Screenshots?" +msgstr "एप्‍लिकेशन को पृष्‍ठभूमि सेट करने की अनुमति दें?" + +#: src/screenshot.c:262 +#, fuzzy +msgid "An application wants to be able to take screenshots at any time." +msgstr "एक एप्लिकेशन आपके माइक्रोफ़ोन का उपयोग करना चाहता है।" + +#: src/screenshot.c:265 src/wallpaper.c:209 +msgid "This permission can be changed at any time from the privacy settings." +msgstr "इस अनुमति को गोपनीयता सेटिंग्स से किसी भी समय बदला जा सकता है।" + +#: src/settings.c:127 +msgid "Requested setting not found" +msgstr "अनुरोधित सेटिंग नहीं मिली" + +#: src/wallpaper.c:192 +msgid "Allow Applications to Set Backgrounds?" +msgstr "एप्‍लिकेशन को पृष्‍ठभूमि सेट करने की अनुमति दें?" + +#: src/wallpaper.c:193 +msgid "An application is requesting to be able to change the background image." +msgstr "एक एप्लिकेशन पृष्ठभूमि छवि को बदलने में सक्षम होने का अनुरोध कर रहा है।" + +#: src/wallpaper.c:205 +#, c-format +msgid "Allow %s to Set Backgrounds?" +msgstr "परमिट %s पृष्ठभूमि सेट करने के लिए?" + +#: src/wallpaper.c:206 +#, c-format +msgid "%s is requesting to be able to change the background image." +msgstr "%s पृष्ठभूमि छवि को बदलने में सक्षम होने का अनुरोध कर रहा है।" diff --git a/po/hr.po b/po/hr.po new file mode 100644 index 0000000..24db4f6 --- /dev/null +++ b/po/hr.po @@ -0,0 +1,190 @@ +# Croatian translation for xdg-desktop-portal. +# Copyright (C) 2017 xdg-desktop-portal's COPYRIGHT HOLDER +# This file is distributed under the same license as the xdg-desktop-portal package. +# Milo Ivir , 2020. +msgid "" +msgstr "" +"Project-Id-Version: xdg-desktop-portal master\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-04 16:59-0300\n" +"PO-Revision-Date: 2020-01-25 16:44+0100\n" +"Last-Translator: Milo Ivir \n" +"Language-Team: \n" +"Language: hr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Generator: Poedit 1.8.12\n" + +#: src/background.c:741 +#, c-format +msgid "Allow %s to run in the background?" +msgstr "Dozvoliti da %s radi u pozadini?" + +#: src/background.c:745 +#, c-format +msgid "%s requests to be started automatically and run in the background." +msgstr "%s traži da se automatski pokrene i da radi u pozadini." + +#: src/background.c:747 +#, c-format +msgid "%s requests to run in the background." +msgstr "%s traži da radi u pozadini." + +#: src/background.c:748 +msgid "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." +msgstr "" +"Dozvolu za „radi u pozadini” je uvijek moguće promijeniti u postavkama " +"programa." + +#: src/background.c:753 +msgid "Don't allow" +msgstr "Nemoj dozvoliti" + +#: src/background.c:754 src/screenshot.c:238 src/wallpaper.c:182 +msgid "Allow" +msgstr "Dozvoli" + +#: src/device.c:116 +msgid "Turn On Microphone?" +msgstr "Uključiti mikrofon?" + +#: src/device.c:117 +msgid "" +"Access to your microphone can be changed at any time from the privacy " +"settings." +msgstr "" +"Pristup tvom mikrofonu je uvijek moguće promijeniti u postavkama privatnosti." + +#: src/device.c:121 +msgid "An application wants to use your microphone." +msgstr "Jedan program želi koristiti tvoj mikrofon." + +#: src/device.c:123 +#, c-format +msgid "%s wants to use your microphone." +msgstr "%s želi koristiti tvoj mikrofon." + +#: src/device.c:129 +msgid "Turn On Speakers?" +msgstr "Uključiti zvučnike?" + +#: src/device.c:130 +msgid "" +"Access to your speakers can be changed at any time from the privacy settings." +msgstr "" +"Pristup tvojim zvučnicima je uvijek moguće promijeniti u postavkama " +"privatnosti." + +#: src/device.c:134 +msgid "An application wants to play sound." +msgstr "Jedan program želi svirati zvuk." + +#: src/device.c:136 +#, c-format +msgid "%s wants to play sound." +msgstr "%s želi svirati zvuk." + +#: src/device.c:142 +msgid "Turn On Camera?" +msgstr "Uključiti kameru?" + +#: src/device.c:143 +msgid "" +"Access to your camera can be changed at any time from the privacy settings." +msgstr "" +"Pristup tvojoj kameri je uvijek moguće promijeniti u postavkama privatnosti." + +#: src/device.c:147 +msgid "An application wants to use your camera." +msgstr "Jedan program želi koristiti tvoju kameru." + +#: src/device.c:149 +#, c-format +msgid "%s wants to use your camera." +msgstr "%s želi koristiti tvoju kameru." + +#: src/location.c:526 +msgid "Deny Access" +msgstr "Zabrani pristup" + +#: src/location.c:528 +msgid "Grant Access" +msgstr "Dozvoli pristup" + +#: src/location.c:534 +msgid "Grant Access to Your Location?" +msgstr "Dozvoliti pristup tvojoj lokaciji?" + +#: src/location.c:535 +msgid "An application wants to use your location." +msgstr "Jedan program želi koristiti tvoju lokaciju." + +#: src/location.c:547 +#, c-format +msgid "Give %s Access to Your Location?" +msgstr "Dozvoliti da %s pristupi tvojoj lokaciji?" + +#: src/location.c:551 +#, c-format +msgid "%s wants to use your location." +msgstr "%s želi koristiti tvoju lokaciju." + +#: src/location.c:554 +msgid "Location access can be changed at any time from the privacy settings." +msgstr "" +"Pristup lokaciji je uvijek moguće promijeniti u postavkama privatnosti." + +#: src/screenshot.c:236 src/wallpaper.c:180 +msgid "Deny" +msgstr "Zabrani" + +#: src/screenshot.c:252 +#, fuzzy, c-format +msgid "Allow %s to Take Screenshots?" +msgstr "Dozvoliti da %s postavi pozadine?" + +#: src/screenshot.c:253 +#, c-format +msgid "%s wants to be able to take screenshots at any time." +msgstr "" + +#: src/screenshot.c:261 +#, fuzzy +msgid "Allow Applications to Take Screenshots?" +msgstr "Dozvoliti programu da postavi pozadine?" + +#: src/screenshot.c:262 +#, fuzzy +msgid "An application wants to be able to take screenshots at any time." +msgstr "Jedan program želi koristiti tvoj mikrofon." + +#: src/screenshot.c:265 src/wallpaper.c:209 +msgid "This permission can be changed at any time from the privacy settings." +msgstr "Ovu dozvolu je uvijek moguće promijeniti u postavkama privatnosti." + +#: src/settings.c:127 +msgid "Requested setting not found" +msgstr "Zatražena postavka nije nađena" + +#: src/wallpaper.c:192 +msgid "Allow Applications to Set Backgrounds?" +msgstr "Dozvoliti programu da postavi pozadine?" + +#: src/wallpaper.c:193 +msgid "An application is requesting to be able to change the background image." +msgstr "Jedan program traži dozvolu za mijenjanje slike pozadine." + +#: src/wallpaper.c:205 +#, c-format +msgid "Allow %s to Set Backgrounds?" +msgstr "Dozvoliti da %s postavi pozadine?" + +#: src/wallpaper.c:206 +#, c-format +msgid "%s is requesting to be able to change the background image." +msgstr "%s traži dozvolu za mijenjanje slike pozadine." diff --git a/po/hu.po b/po/hu.po new file mode 100644 index 0000000..b351a22 --- /dev/null +++ b/po/hu.po @@ -0,0 +1,193 @@ +# Hungarian translation for xdg-sesktop-portal. +# Copyright (C) 2016, 2019, 2022 Free Software Foundation, Inc. +# This file is distributed under the same license as the xdg-sesktop-portal package. +# +# Balázs Úr , 2016, 2019, 2022. +msgid "" +msgstr "" +"Project-Id-Version: xdg-desktop-portal\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-04 16:59-0300\n" +"PO-Revision-Date: 2022-09-21 22:58+0200\n" +"Last-Translator: Balázs Úr \n" +"Language-Team: Hungarian \n" +"Language: hu\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 19.12.3\n" + +#: src/background.c:741 +#, c-format +msgid "Allow %s to run in the background?" +msgstr "Engedélyezi a(z) %s számára, hogy a háttérben fusson?" + +#: src/background.c:745 +#, c-format +msgid "%s requests to be started automatically and run in the background." +msgstr "A(z) %s azt kéri, hogy automatikusan elinduljon és a háttérben fusson." + +#: src/background.c:747 +#, c-format +msgid "%s requests to run in the background." +msgstr "A(z) %s azt kéri, hogy a háttérben fusson." + +#: src/background.c:748 +msgid "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." +msgstr "" +"A „háttérben való futás” engedély bármikor megváltoztatható az alkalmazás " +"beállításaiban." + +#: src/background.c:753 +msgid "Don't allow" +msgstr "Ne engedélyezze" + +#: src/background.c:754 src/screenshot.c:238 src/wallpaper.c:182 +msgid "Allow" +msgstr "Engedélyezés" + +#: src/device.c:116 +msgid "Turn On Microphone?" +msgstr "Mikrofon bekapcsolása?" + +#: src/device.c:117 +msgid "" +"Access to your microphone can be changed at any time from the privacy " +"settings." +msgstr "" +"A mikrofonhoz való hozzáférés bármikor megváltoztatható az adatvédelmi " +"beállításokban." + +#: src/device.c:121 +msgid "An application wants to use your microphone." +msgstr "Egy alkalmazás használni szeretné a mikrofonját." + +#: src/device.c:123 +#, c-format +msgid "%s wants to use your microphone." +msgstr "A(z) %s használni szeretné a mikrofonját." + +#: src/device.c:129 +msgid "Turn On Speakers?" +msgstr "Hangszórók bekapcsolása?" + +#: src/device.c:130 +msgid "" +"Access to your speakers can be changed at any time from the privacy settings." +msgstr "" +"A hangszórókhoz való hozzáférés bármikor megváltoztatható az adatvédelmi " +"beállításokban." + +#: src/device.c:134 +msgid "An application wants to play sound." +msgstr "Egy alkalmazás hangot szeretne lejátszani." + +#: src/device.c:136 +#, c-format +msgid "%s wants to play sound." +msgstr "A(z) %s hangot szeretne lejátszani." + +#: src/device.c:142 +msgid "Turn On Camera?" +msgstr "Kamera bekapcsolása?" + +#: src/device.c:143 +msgid "" +"Access to your camera can be changed at any time from the privacy settings." +msgstr "" +"A kamerához való hozzáférés bármikor megváltoztatható az adatvédelmi " +"beállításokban." + +#: src/device.c:147 +msgid "An application wants to use your camera." +msgstr "Egy alkalmazás használni szeretné a kameráját." + +#: src/device.c:149 +#, c-format +msgid "%s wants to use your camera." +msgstr "A(z) %s használni szeretné a kameráját." + +#: src/location.c:526 +msgid "Deny Access" +msgstr "Hozzáférés megtagadása" + +#: src/location.c:528 +msgid "Grant Access" +msgstr "Hozzáférés megadása" + +#: src/location.c:534 +msgid "Grant Access to Your Location?" +msgstr "Hozzáférést ad a tartózkodási helyéhez?" + +#: src/location.c:535 +msgid "An application wants to use your location." +msgstr "Egy alkalmazás használni szeretné a tartózkodási helyét." + +#: src/location.c:547 +#, c-format +msgid "Give %s Access to Your Location?" +msgstr "Hozzáférést ad a(z) %s számára a tartózkodási helyéhez?" + +#: src/location.c:551 +#, c-format +msgid "%s wants to use your location." +msgstr "A(z) %s használni szeretné a tartózkodási helyét." + +#: src/location.c:554 +msgid "Location access can be changed at any time from the privacy settings." +msgstr "" +"A tartózkodási helyhez való hozzáférés bármikor megváltoztatható az " +"adatvédelmi beállításokban." + +#: src/screenshot.c:236 src/wallpaper.c:180 +msgid "Deny" +msgstr "Elutasítás" + +#: src/screenshot.c:252 +#, c-format +msgid "Allow %s to Take Screenshots?" +msgstr "Engedélyezi a(z) %s számára, hogy képernyőképeket készítsen?" + +#: src/screenshot.c:253 +#, c-format +msgid "%s wants to be able to take screenshots at any time." +msgstr "A(z) %s azt szeretné, hogy bármikor készíthessen képernyőképeket." + +#: src/screenshot.c:261 +msgid "Allow Applications to Take Screenshots?" +msgstr "Engedélyezi az alkalmazások számára, hogy képernyőképeket készítsenek?" + +#: src/screenshot.c:262 +msgid "An application wants to be able to take screenshots at any time." +msgstr "" +"Egy alkalmazás azt szeretné, hogy bármikor készíthessen képernyőképeket." + +#: src/screenshot.c:265 src/wallpaper.c:209 +msgid "This permission can be changed at any time from the privacy settings." +msgstr "" +"Ez az engedély bármikor megváltoztatható az adatvédelmi beállításokban." + +#: src/settings.c:127 +msgid "Requested setting not found" +msgstr "A kért beállítás nem található" + +#: src/wallpaper.c:192 +msgid "Allow Applications to Set Backgrounds?" +msgstr "Engedélyezi az alkalmazások számára, hogy háttereket állítsanak be?" + +#: src/wallpaper.c:193 +msgid "An application is requesting to be able to change the background image." +msgstr "Egy alkalmazás azt kéri, hogy megváltoztathassa a háttérképet." + +#: src/wallpaper.c:205 +#, c-format +msgid "Allow %s to Set Backgrounds?" +msgstr "Engedélyezi a(z) %s számára, hogy háttereket állítson be?" + +#: src/wallpaper.c:206 +#, c-format +msgid "%s is requesting to be able to change the background image." +msgstr "A(z) %s azt kéri, hogy megváltoztathassa a háttérképet." diff --git a/po/id.po b/po/id.po new file mode 100644 index 0000000..6b1a778 --- /dev/null +++ b/po/id.po @@ -0,0 +1,186 @@ +# Indonesian translation for xdg-desktop-portal. +# Copyright (C) 2017 xdg-desktop-portal's COPYRIGHT HOLDER +# This file is distributed under the same license as the xdg-desktop-portal package. +# Kukuh Syafaat , 2017-2020, 2022. +# +msgid "" +msgstr "" +"Project-Id-Version: xdg-desktop-portal master\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-04 16:59-0300\n" +"PO-Revision-Date: 2022-11-30 10:52+0700\n" +"Last-Translator: Kukuh Syafaat \n" +"Language-Team: Indonesian \n" +"Language: id\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 3.2.1\n" + +#: src/background.c:741 +#, c-format +msgid "Allow %s to run in the background?" +msgstr "Izinkan %s berjalan di latar belakang?" + +#: src/background.c:745 +#, c-format +msgid "%s requests to be started automatically and run in the background." +msgstr "" +"%s meminta untuk dimulai secara otomatis dan dijalankan di latar belakang." + +#: src/background.c:747 +#, c-format +msgid "%s requests to run in the background." +msgstr "%s meminta untuk berjalan di latar belakang." + +#: src/background.c:748 +msgid "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." +msgstr "" +"Izin 'berjalan di latar belakang' dapat diubah kapan saja dari pengaturan " +"aplikasi." + +#: src/background.c:753 +msgid "Don't allow" +msgstr "Jangan izinkan" + +#: src/background.c:754 src/screenshot.c:238 src/wallpaper.c:182 +msgid "Allow" +msgstr "Izinkan" + +#: src/device.c:116 +msgid "Turn On Microphone?" +msgstr "Nyalakan Mikrofon?" + +#: src/device.c:117 +msgid "" +"Access to your microphone can be changed at any time from the privacy " +"settings." +msgstr "" +"Akses ke mikrofon Anda dapat diubah sewaktu-waktu dari pengaturan privasi." + +#: src/device.c:121 +msgid "An application wants to use your microphone." +msgstr "Aplikasi ingin menggunakan mikrofon Anda." + +#: src/device.c:123 +#, c-format +msgid "%s wants to use your microphone." +msgstr "%s ingin menggunakan mikrofon Anda." + +#: src/device.c:129 +msgid "Turn On Speakers?" +msgstr "Nyalakan Speaker?" + +#: src/device.c:130 +msgid "" +"Access to your speakers can be changed at any time from the privacy settings." +msgstr "" +"Akses ke speaker Anda dapat berubah sewaktu-waktu dari pengaturan privasi." + +#: src/device.c:134 +msgid "An application wants to play sound." +msgstr "Aplikasi ingin memutar suara." + +#: src/device.c:136 +#, c-format +msgid "%s wants to play sound." +msgstr "%s ingin memutar suara." + +#: src/device.c:142 +msgid "Turn On Camera?" +msgstr "Nyalakan Kamera?" + +#: src/device.c:143 +msgid "" +"Access to your camera can be changed at any time from the privacy settings." +msgstr "" +"Akses ke kamera Anda dapat diubah sewaktu-waktu dari pengaturan privasi." + +#: src/device.c:147 +msgid "An application wants to use your camera." +msgstr "Aplikasi ingin menggunakan kamera Anda." + +#: src/device.c:149 +#, c-format +msgid "%s wants to use your camera." +msgstr "%s ingin menggunakan kamera Anda." + +#: src/location.c:526 +msgid "Deny Access" +msgstr "Tolak Akses" + +#: src/location.c:528 +msgid "Grant Access" +msgstr "Beri Akses" + +#: src/location.c:534 +msgid "Grant Access to Your Location?" +msgstr "Beri Akses ke Lokasi Anda?" + +#: src/location.c:535 +msgid "An application wants to use your location." +msgstr "Sebuah aplikasi ingin menggunakan lokasi Anda." + +#: src/location.c:547 +#, c-format +msgid "Give %s Access to Your Location?" +msgstr "Beri Akses %s ke Lokasi Anda?" + +#: src/location.c:551 +#, c-format +msgid "%s wants to use your location." +msgstr "%s ingin menggunakan lokasi Anda." + +#: src/location.c:554 +msgid "Location access can be changed at any time from the privacy settings." +msgstr "Akses lokasi dapat diubah setiap saat dari pengaturan privasi." + +#: src/screenshot.c:236 src/wallpaper.c:180 +msgid "Deny" +msgstr "Tolak" + +#: src/screenshot.c:252 +#, c-format +msgid "Allow %s to Take Screenshots?" +msgstr "Izinkan %s Mengambil Cuplikan Layar?" + +#: src/screenshot.c:253 +#, c-format +msgid "%s wants to be able to take screenshots at any time." +msgstr "%s ingin dapat mengambil cuplikan layar kapan saja." + +#: src/screenshot.c:261 +msgid "Allow Applications to Take Screenshots?" +msgstr "Izinkan Aplikasi Mengambil Cuplikan Layar?" + +#: src/screenshot.c:262 +msgid "An application wants to be able to take screenshots at any time." +msgstr "Sebuah aplikasi ingin dapat mengambil cuplikan layar kapan saja." + +#: src/screenshot.c:265 src/wallpaper.c:209 +msgid "This permission can be changed at any time from the privacy settings." +msgstr "Izin ini dapat diubah kapan saja dari pengaturan privasi." + +#: src/settings.c:127 +msgid "Requested setting not found" +msgstr "Pengaturan yang diminta tidak ditemukan" + +#: src/wallpaper.c:192 +msgid "Allow Applications to Set Backgrounds?" +msgstr "Izinkan Aplikasi untuk Mengatur Latar Belakang?" + +#: src/wallpaper.c:193 +msgid "An application is requesting to be able to change the background image." +msgstr "Aplikasi meminta untuk dapat mengubah gambar latar belakang." + +#: src/wallpaper.c:205 +#, c-format +msgid "Allow %s to Set Backgrounds?" +msgstr "Izinkan %s untuk Mengatur Latar Belakang?" + +#: src/wallpaper.c:206 +#, c-format +msgid "%s is requesting to be able to change the background image." +msgstr "%s meminta untuk dapat mengubah gambar latar belakang." diff --git a/po/it.po b/po/it.po new file mode 100644 index 0000000..905bbec --- /dev/null +++ b/po/it.po @@ -0,0 +1,197 @@ +# Italian translation for xdg-desktop-portal. +# Copyright (C) 2017, 2018, 2019, 2020 xdg-desktop-portal's COPYRIGHT HOLDER +# This file is distributed under the same license as the xdg-desktop-portal package. +# Milo Casagrande , 2017, 2018, 2019, 2020. +# +msgid "" +msgstr "" +"Project-Id-Version: xdg-desktop-portal master\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-04 16:59-0300\n" +"PO-Revision-Date: 2020-02-10 16:11+0100\n" +"Last-Translator: Milo Casagrande \n" +"Language-Team: Italian \n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 2.2.4\n" + +#: src/background.c:741 +#, c-format +msgid "Allow %s to run in the background?" +msgstr "Consentire a %s di andare in esecuzione sullo sfondo?" + +#: src/background.c:745 +#, c-format +msgid "%s requests to be started automatically and run in the background." +msgstr "" +"L'applicazione %s richiede di essere avviata automaticamente e di essere " +"eseguita sullo sfondo." + +#: src/background.c:747 +#, c-format +msgid "%s requests to run in the background." +msgstr "L'applicazione %s richiede di essere eseguita sullo sfondo." + +#: src/background.c:748 +msgid "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." +msgstr "" +"Il permesso «esegui sullo sfondo» può essere modificato in qualsiasi momento " +"attraverso le impostazioni dell'applicazione." + +#: src/background.c:753 +msgid "Don't allow" +msgstr "Non consentire" + +#: src/background.c:754 src/screenshot.c:238 src/wallpaper.c:182 +msgid "Allow" +msgstr "Consenti" + +#: src/device.c:116 +msgid "Turn On Microphone?" +msgstr "Accendere il microfono?" + +#: src/device.c:117 +msgid "" +"Access to your microphone can be changed at any time from the privacy " +"settings." +msgstr "" +"L'accesso al microfono può essere modificato in qualsiasi momento attraverso " +"le impostazioni della privacy." + +#: src/device.c:121 +msgid "An application wants to use your microphone." +msgstr "Un'applicazione vuole utilizzare il microfono." + +#: src/device.c:123 +#, c-format +msgid "%s wants to use your microphone." +msgstr "%s vuole utilizzare il microfono." + +#: src/device.c:129 +msgid "Turn On Speakers?" +msgstr "Accendere gli altoparlanti?" + +#: src/device.c:130 +msgid "" +"Access to your speakers can be changed at any time from the privacy settings." +msgstr "" +"L'accesso al microfono può essere modificato in qualsiasi momento attraverso " +"le impostazioni della privacy." + +#: src/device.c:134 +msgid "An application wants to play sound." +msgstr "Un'applicazione vuole riprodurre un suono." + +#: src/device.c:136 +#, c-format +msgid "%s wants to play sound." +msgstr "%s vuole riprodurre un suono." + +#: src/device.c:142 +msgid "Turn On Camera?" +msgstr "Accendere la videocamera?" + +#: src/device.c:143 +msgid "" +"Access to your camera can be changed at any time from the privacy settings." +msgstr "" +"L'accesso alla videocamera può essere modificato in qualsiasi momento " +"attraverso le impostazioni della privacy." + +#: src/device.c:147 +msgid "An application wants to use your camera." +msgstr "Un'applicazione vuole utilizzare la videocamera." + +#: src/device.c:149 +#, c-format +msgid "%s wants to use your camera." +msgstr "%s vuole utilizzare la videocamera." + +#: src/location.c:526 +msgid "Deny Access" +msgstr "Nega accesso" + +#: src/location.c:528 +msgid "Grant Access" +msgstr "Consenti accesso" + +#: src/location.c:534 +msgid "Grant Access to Your Location?" +msgstr "Consentire l'accesso alla propria posizione?" + +#: src/location.c:535 +msgid "An application wants to use your location." +msgstr "Un'applicazione vuole utilizzare la propria posizione." + +#: src/location.c:547 +#, c-format +msgid "Give %s Access to Your Location?" +msgstr "Consentire a %s di accedere alla propria posizione?" + +#: src/location.c:551 +#, c-format +msgid "%s wants to use your location." +msgstr "%s vuole utilizzare la propria posizione." + +#: src/location.c:554 +msgid "Location access can be changed at any time from the privacy settings." +msgstr "" +"L'accesso alla posizione può essere modificato in qualsiasi momento " +"attraverso le impostazioni della privacy." + +#: src/screenshot.c:236 src/wallpaper.c:180 +msgid "Deny" +msgstr "Nega" + +#: src/screenshot.c:252 +#, fuzzy, c-format +msgid "Allow %s to Take Screenshots?" +msgstr "Consentire a %s di impostare lo sfondo?" + +#: src/screenshot.c:253 +#, c-format +msgid "%s wants to be able to take screenshots at any time." +msgstr "" + +#: src/screenshot.c:261 +#, fuzzy +msgid "Allow Applications to Take Screenshots?" +msgstr "Consentire alle applicazioni di impostare lo sfondo?" + +#: src/screenshot.c:262 +#, fuzzy +msgid "An application wants to be able to take screenshots at any time." +msgstr "Un'applicazione vuole utilizzare il microfono." + +#: src/screenshot.c:265 src/wallpaper.c:209 +msgid "This permission can be changed at any time from the privacy settings." +msgstr "" +"Questa impostazione può essere modificata in qualsiasi momento attraverso le " +"impostazioni della privacy." + +#: src/settings.c:127 +msgid "Requested setting not found" +msgstr "La risorsa richiesta non è stata trovata" + +#: src/wallpaper.c:192 +msgid "Allow Applications to Set Backgrounds?" +msgstr "Consentire alle applicazioni di impostare lo sfondo?" + +#: src/wallpaper.c:193 +msgid "An application is requesting to be able to change the background image." +msgstr "Un'applicazione richiede di poter cambiare l'immagine di sfondo." + +#: src/wallpaper.c:205 +#, c-format +msgid "Allow %s to Set Backgrounds?" +msgstr "Consentire a %s di impostare lo sfondo?" + +#: src/wallpaper.c:206 +#, c-format +msgid "%s is requesting to be able to change the background image." +msgstr "L'applicazione %s richiede di poter cambiare l'immagine di sfondo." diff --git a/po/ka.po b/po/ka.po new file mode 100644 index 0000000..26b422a --- /dev/null +++ b/po/ka.po @@ -0,0 +1,193 @@ +# Georgian translation for xdg-desktop-portal. +# Copyright (C) 2023 xdg-desktop-portal's authors. +# This file is distributed under the same license as the xdg-desktop-portal package. +# Temuri Doghonadze , 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: xdg-desktop-portal master\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-04 16:59-0300\n" +"PO-Revision-Date: 2023-02-25 14:53+0100\n" +"Last-Translator: Temuri Doghonadze \n" +"Language-Team: Georgian <(nothing)>\n" +"Language: ka\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.2.2\n" + +#: src/background.c:741 +#, c-format +msgid "Allow %s to run in the background?" +msgstr "დავუშვა %s-ის ფონურ რეჟიმში გაშვება?" + +#: src/background.c:745 +#, c-format +msgid "%s requests to be started automatically and run in the background." +msgstr "%s ავტომატურად გაშვებას და ფონურ რეჟიმს ითხოვს." + +#: src/background.c:747 +#, c-format +msgid "%s requests to run in the background." +msgstr "%s ითხოვს გაშვებას ფონზე." + +#: src/background.c:748 +msgid "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." +msgstr "" +"„ფონში გაშვების“ ნებართვა ნებისმიერ დროს აპლიკაციის პარამეტრებიდან შეიძლება " +"შეიცვალოს." + +#: src/background.c:753 +msgid "Don't allow" +msgstr "არ დაშვება" + +#: src/background.c:754 src/screenshot.c:238 src/wallpaper.c:182 +msgid "Allow" +msgstr "დაშვება" + +#: src/device.c:116 +msgid "Turn On Microphone?" +msgstr "ჩავრთო მიკროფონი?" + +#: src/device.c:117 +msgid "" +"Access to your microphone can be changed at any time from the privacy " +"settings." +msgstr "" +"თქვენს მიკროფონზე წვდომა ნებისმიერ დროს კონფიდენციალურობის პარამეტრებიდან " +"შეიძლება შეიცვალოს." + +#: src/device.c:121 +msgid "An application wants to use your microphone." +msgstr "აპლიკაციას სურს გამოიყენოს თქვენი მიკროფონი." + +#: src/device.c:123 +#, c-format +msgid "%s wants to use your microphone." +msgstr "%s-ს სურს თქვენი მიკროფონი გამოიყენოს." + +#: src/device.c:129 +msgid "Turn On Speakers?" +msgstr "ჩავრთო დინამიკები?" + +#: src/device.c:130 +msgid "" +"Access to your speakers can be changed at any time from the privacy settings." +msgstr "" +"თქვენს დინამიკებზე წვდომა შეიძლება შეიცვალოს ნებისმიერ დროს " +"კონფიდენციალურობის პარამეტრებიდან." + +#: src/device.c:134 +msgid "An application wants to play sound." +msgstr "აპლიკაციას ხმის გამოცემა სურს." + +#: src/device.c:136 +#, c-format +msgid "%s wants to play sound." +msgstr "%s-ს ხმის გამოცემა სურს." + +#: src/device.c:142 +msgid "Turn On Camera?" +msgstr "ჩავრთო კამერა?" + +#: src/device.c:143 +msgid "" +"Access to your camera can be changed at any time from the privacy settings." +msgstr "" +"თქვენს კამერაზე წვდომა ნებისმიერ დროს შეიძლება შეიცვალოს კონფიდენციალურობის " +"პარამეტრებიდან." + +#: src/device.c:147 +msgid "An application wants to use your camera." +msgstr "აპლიკაციას სურს გამოიყენოს თქვენი კამერა." + +#: src/device.c:149 +#, c-format +msgid "%s wants to use your camera." +msgstr "%s-ს სურს გამოიყენოს თქვენი კამერა." + +#: src/location.c:526 +msgid "Deny Access" +msgstr "წვდომის უარყოფა" + +#: src/location.c:528 +msgid "Grant Access" +msgstr "წვდომის მინიჭება" + +#: src/location.c:534 +msgid "Grant Access to Your Location?" +msgstr "მივანიჭოთ თქვენს მდებარეობაზე წვდომა?" + +#: src/location.c:535 +msgid "An application wants to use your location." +msgstr "აპლიკაციას სურს გამოიყენოს თქვენი მდებარეობა." + +#: src/location.c:547 +#, c-format +msgid "Give %s Access to Your Location?" +msgstr "მივანიჭოთ %s-ს თქვენს მდებარეობაზე წვდომა?" + +#: src/location.c:551 +#, c-format +msgid "%s wants to use your location." +msgstr "%s-ს სურს გამოიყენოს თქვენი მდებარეობა." + +#: src/location.c:554 +msgid "Location access can be changed at any time from the privacy settings." +msgstr "" +"მდებარეობაზე წვდომა შეიძლება შეიცვალოს ნებისმიერ დროს კონფიდენციალურობის " +"პარამეტრებიდან." + +#: src/screenshot.c:236 src/wallpaper.c:180 +msgid "Deny" +msgstr "უარყოფა" + +#: src/screenshot.c:252 +#, c-format +msgid "Allow %s to Take Screenshots?" +msgstr "მიეცეს %s-ს ეკრანის ანაბეჭდების გადაღების უფლება?" + +#: src/screenshot.c:253 +#, c-format +msgid "%s wants to be able to take screenshots at any time." +msgstr "%s-ს სურს ეკრანის ანაბეჭდის სურვილისამებრ გადაღების უფლება მიიღოს." + +#: src/screenshot.c:261 +msgid "Allow Applications to Take Screenshots?" +msgstr "მიეცეს აპლიკაციას ეკრანის ანაბეჭდების გადაღების უფლება?" + +#: src/screenshot.c:262 +msgid "An application wants to be able to take screenshots at any time." +msgstr "" +"აპლიკაციას სურს ეკრანის ანაბეჭდის სურვილისამებრ გადაღების უფლება მიიღოს." + +#: src/screenshot.c:265 src/wallpaper.c:209 +msgid "This permission can be changed at any time from the privacy settings." +msgstr "" +"წვდომის შეცვლა შესაძლებელია ნებისმიერ დროს კონფიდენციალურობის პარამეტრებიდან." + +#: src/settings.c:127 +msgid "Requested setting not found" +msgstr "მოთხოვნილი პარამეტრი ვერ მოიძებნა" + +#: src/wallpaper.c:192 +msgid "Allow Applications to Set Backgrounds?" +msgstr "მიეცეს აპლიკაციებს ფონის სურათის შეცვლის უფლება?" + +#: src/wallpaper.c:193 +msgid "An application is requesting to be able to change the background image." +msgstr "აპლიკაცია ფონის სურათის შეცვლის უფლებას მოითხოვს." + +#: src/wallpaper.c:205 +#, c-format +msgid "Allow %s to Set Backgrounds?" +msgstr "მიეცეს %s-ს ფონის სურათის შეცვლის უფლება?" + +#: src/wallpaper.c:206 +#, c-format +msgid "%s is requesting to be able to change the background image." +msgstr "%s ფონის სურათის შეცვლის უფლებას მოითხოვს." diff --git a/po/lt.po b/po/lt.po new file mode 100644 index 0000000..649fec1 --- /dev/null +++ b/po/lt.po @@ -0,0 +1,195 @@ +# Lithuanian translation for xdg-desktop-portal. +# Copyright (C) 2018 xdg-desktop-portal's COPYRIGHT HOLDER +# This file is distributed under the same license as the xdg-desktop-portal package. +# Moo, 2018,2019, 2020. +# +msgid "" +msgstr "" +"Project-Id-Version: xdg-desktop-portal master\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-04 16:59-0300\n" +"PO-Revision-Date: 2020-03-07 20:26+0200\n" +"Last-Translator: Moo\n" +"Language-Team: Lithuanian \n" +"Language: lt\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"(n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Generator: Poedit 2.3\n" + +#: src/background.c:741 +#, c-format +msgid "Allow %s to run in the background?" +msgstr "Leisti %s vykdyti fone?" + +#: src/background.c:745 +#, c-format +msgid "%s requests to be started automatically and run in the background." +msgstr "%s prašo, kad būtų paleidžiama automatiškai ir vykdoma fone." + +#: src/background.c:747 +#, c-format +msgid "%s requests to run in the background." +msgstr "%s prašo, kad būtų vykdoma fone." + +#: src/background.c:748 +msgid "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." +msgstr "" +"Leidimas \"vykdyti fone\" bet kuriuo metu gali būti pakeistas programos " +"nustatymuose." + +#: src/background.c:753 +msgid "Don't allow" +msgstr "Neleisti" + +#: src/background.c:754 src/screenshot.c:238 src/wallpaper.c:182 +msgid "Allow" +msgstr "Leisti" + +#: src/device.c:116 +msgid "Turn On Microphone?" +msgstr "Įjungti mikrofoną?" + +#: src/device.c:117 +msgid "" +"Access to your microphone can be changed at any time from the privacy " +"settings." +msgstr "" +"Prieiga prie jūsų mikrofono bet kuriuo metu gali būti pakeista privatumo " +"nustatymuose." + +#: src/device.c:121 +msgid "An application wants to use your microphone." +msgstr "Programa nori naudoti jūsų mikrofoną." + +#: src/device.c:123 +#, c-format +msgid "%s wants to use your microphone." +msgstr "%s nori naudoti jūsų mikrofoną." + +#: src/device.c:129 +msgid "Turn On Speakers?" +msgstr "Įjungti garsiakalbius?" + +#: src/device.c:130 +msgid "" +"Access to your speakers can be changed at any time from the privacy settings." +msgstr "" +"Prieiga prie jūsų garsiakalbių bet kuriuo metu gali būti pakeista privatumo " +"nustatymuose." + +#: src/device.c:134 +msgid "An application wants to play sound." +msgstr "Programa nori atkurti garsą." + +#: src/device.c:136 +#, c-format +msgid "%s wants to play sound." +msgstr "%s nori atkurti garsą." + +#: src/device.c:142 +msgid "Turn On Camera?" +msgstr "Įjungti kamerą?" + +#: src/device.c:143 +msgid "" +"Access to your camera can be changed at any time from the privacy settings." +msgstr "" +"Prieiga prie jūsų kameros bet kuriuo metu gali būti pakeista privatumo " +"nustatymuose." + +#: src/device.c:147 +msgid "An application wants to use your camera." +msgstr "Programa nori naudoti jūsų kamerą." + +#: src/device.c:149 +#, c-format +msgid "%s wants to use your camera." +msgstr "%s nori naudoti jūsų kamerą." + +#: src/location.c:526 +msgid "Deny Access" +msgstr "Drausti prieigą" + +#: src/location.c:528 +msgid "Grant Access" +msgstr "Suteikti prieigą" + +#: src/location.c:534 +msgid "Grant Access to Your Location?" +msgstr "Suteikti prieigą prie jūsų buvimo vietos?" + +#: src/location.c:535 +msgid "An application wants to use your location." +msgstr "Programa nori naudoti jūsų buvimo vietą." + +#: src/location.c:547 +#, c-format +msgid "Give %s Access to Your Location?" +msgstr "Suteikti %s prieigą prie jūsų buvimo vietos?" + +#: src/location.c:551 +#, c-format +msgid "%s wants to use your location." +msgstr "%s nori naudoti jūsų buvimo vietą." + +#: src/location.c:554 +msgid "Location access can be changed at any time from the privacy settings." +msgstr "" +"Prieiga prie jūsų buvimo vietos bet kuriuo metu gali būti pakeista privatumo " +"nustatymuose." + +#: src/screenshot.c:236 src/wallpaper.c:180 +msgid "Deny" +msgstr "Drausti" + +#: src/screenshot.c:252 +#, fuzzy, c-format +msgid "Allow %s to Take Screenshots?" +msgstr "Leisti %s nustatyti fonus?" + +#: src/screenshot.c:253 +#, c-format +msgid "%s wants to be able to take screenshots at any time." +msgstr "" + +#: src/screenshot.c:261 +#, fuzzy +msgid "Allow Applications to Take Screenshots?" +msgstr "Leisti programoms nustatyti fonus?" + +#: src/screenshot.c:262 +#, fuzzy +msgid "An application wants to be able to take screenshots at any time." +msgstr "Programa nori naudoti jūsų mikrofoną." + +#: src/screenshot.c:265 src/wallpaper.c:209 +msgid "This permission can be changed at any time from the privacy settings." +msgstr "" +"Šis leidimas bet kuriuo metu gali būti pakeistas privatumo nustatymuose." + +#: src/settings.c:127 +msgid "Requested setting not found" +msgstr "Užklaustas nustatymas nerastas" + +#: src/wallpaper.c:192 +msgid "Allow Applications to Set Backgrounds?" +msgstr "Leisti programoms nustatyti fonus?" + +#: src/wallpaper.c:193 +msgid "An application is requesting to be able to change the background image." +msgstr "Programa prašo galimybės keisti fono paveikslą." + +#: src/wallpaper.c:205 +#, c-format +msgid "Allow %s to Set Backgrounds?" +msgstr "Leisti %s nustatyti fonus?" + +#: src/wallpaper.c:206 +#, c-format +msgid "%s is requesting to be able to change the background image." +msgstr "%s prašo galimybės keisti fono paveikslą." diff --git a/po/meson.build b/po/meson.build new file mode 100644 index 0000000..5eab52e --- /dev/null +++ b/po/meson.build @@ -0,0 +1 @@ +i18n.gettext(meson.project_name(), preset: 'glib') \ No newline at end of file diff --git a/po/nl.po b/po/nl.po new file mode 100644 index 0000000..33aa44e --- /dev/null +++ b/po/nl.po @@ -0,0 +1,201 @@ +# German translation for xdg-desktop-portal. +# Copyright (C) 2016 xdg-desktop-portal's COPYRIGHT HOLDER +# This file is distributed under the same license as the xdg-desktop-portal package. +# Benedikt M. Thoma , 2016. +# Mario Blättermann , 2016. +# +msgid "" +msgstr "" +"Project-Id-Version: xdg-desktop-portal master\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-04 16:59-0300\n" +"PO-Revision-Date: 2021-07-18 14:05+0200\n" +"Last-Translator: Heimen Stoffels \n" +"Language-Team: Dutch\n" +"Language: nl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.0\n" + +#: src/background.c:741 +#, c-format +msgid "Allow %s to run in the background?" +msgstr "Wilt u %s toestaan om op de achtergrond te worden uitgevoerd?" + +#: src/background.c:745 +#, c-format +msgid "%s requests to be started automatically and run in the background." +msgstr "" +"%s verzoekt om automatisch te mogen worden opgestart en uitgevoerd te mogen " +"worden op de achtergrond." + +#: src/background.c:747 +#, c-format +msgid "%s requests to run in the background." +msgstr "" +"%s verzoekt om automatisch te mogen worden uitgevoerd op de achtergrond." + +#: src/background.c:748 +msgid "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." +msgstr "" +"Het toegangsrecht ‘uitvoeren op achtergrond’ kan te allen tijde worden " +"ingetrokken in de toepassingsvoorkeuren." + +#: src/background.c:753 +msgid "Don't allow" +msgstr "Niet toestaan" + +#: src/background.c:754 src/screenshot.c:238 src/wallpaper.c:182 +msgid "Allow" +msgstr "Toestaan" + +#: src/device.c:116 +msgid "Turn On Microphone?" +msgstr "Microfoon aanzetten?" + +#: src/device.c:117 +msgid "" +"Access to your microphone can be changed at any time from the privacy " +"settings." +msgstr "" +"De toegang tot uw microfoon kan te allen tijde worden aangepast in de " +"privacyvoorkeuren." + +#: src/device.c:121 +msgid "An application wants to use your microphone." +msgstr "Een toepassing wil uw microfoon gebruiken." + +#: src/device.c:123 +#, c-format +msgid "%s wants to use your microphone." +msgstr "%s wil uw microfoon gebruiken." + +#: src/device.c:129 +msgid "Turn On Speakers?" +msgstr "Luidsprekers aanzetten?" + +#: src/device.c:130 +msgid "" +"Access to your speakers can be changed at any time from the privacy settings." +msgstr "" +"De toegang tot uw luidsprekers kan te allen tijde worden aangepast in de " +"privacyvoorkeuren." + +#: src/device.c:134 +msgid "An application wants to play sound." +msgstr "Een toepassing wil geluid afspelen." + +#: src/device.c:136 +#, c-format +msgid "%s wants to play sound." +msgstr "%s wil geluid afspelen." + +#: src/device.c:142 +msgid "Turn On Camera?" +msgstr "Camera aanzetten?" + +#: src/device.c:143 +msgid "" +"Access to your camera can be changed at any time from the privacy settings." +msgstr "" +"De toegang tot uw camera kan te allen tijde worden aangepast in de " +"privacyvoorkeuren." + +#: src/device.c:147 +msgid "An application wants to use your camera." +msgstr "Een toepassing wil uw camera gebruiken." + +#: src/device.c:149 +#, c-format +msgid "%s wants to use your camera." +msgstr "%s wil uw camera gebruiken." + +#: src/location.c:526 +msgid "Deny Access" +msgstr "Toegang weigeren" + +#: src/location.c:528 +msgid "Grant Access" +msgstr "Toegang verlenen" + +#: src/location.c:534 +msgid "Grant Access to Your Location?" +msgstr "Toegang verlenen tot uw locatie?" + +#: src/location.c:535 +msgid "An application wants to use your location." +msgstr "Een toepassing wil uw locatie opvragen." + +#: src/location.c:547 +#, c-format +msgid "Give %s Access to Your Location?" +msgstr "Wilt u %s toegang verlenen tot uw locatie?" + +#: src/location.c:551 +#, c-format +msgid "%s wants to use your location." +msgstr "%s wil uw locatie opvragen." + +#: src/location.c:554 +msgid "Location access can be changed at any time from the privacy settings." +msgstr "" +"De toegang tot uw locatie kan te allen tijde worden aangepast in de " +"privacyvoorkeuren." + +#: src/screenshot.c:236 src/wallpaper.c:180 +msgid "Deny" +msgstr "Weigeren" + +#: src/screenshot.c:252 +#, fuzzy, c-format +msgid "Allow %s to Take Screenshots?" +msgstr "%s toestaan achtergronden in te stellen?" + +#: src/screenshot.c:253 +#, c-format +msgid "%s wants to be able to take screenshots at any time." +msgstr "" + +#: src/screenshot.c:261 +#, fuzzy +msgid "Allow Applications to Take Screenshots?" +msgstr "Toepassingen toestaan achtergronden in te stellen?" + +#: src/screenshot.c:262 +#, fuzzy +msgid "An application wants to be able to take screenshots at any time." +msgstr "Een toepassing wil uw microfoon gebruiken." + +#: src/screenshot.c:265 src/wallpaper.c:209 +msgid "This permission can be changed at any time from the privacy settings." +msgstr "" +"Dit toegangsrecht kan te allen tijde worden aangepast in de " +"privacyvoorkeuren." + +#: src/settings.c:127 +msgid "Requested setting not found" +msgstr "De opgevraagde instelling bestaat niet" + +#: src/wallpaper.c:192 +msgid "Allow Applications to Set Backgrounds?" +msgstr "Toepassingen toestaan achtergronden in te stellen?" + +#: src/wallpaper.c:193 +msgid "An application is requesting to be able to change the background image." +msgstr "" +"Een toepassing verzoekt de mogelijkheid om uw achtergrondafbeelding te " +"wijzigen." + +#: src/wallpaper.c:205 +#, c-format +msgid "Allow %s to Set Backgrounds?" +msgstr "%s toestaan achtergronden in te stellen?" + +#: src/wallpaper.c:206 +#, c-format +msgid "%s is requesting to be able to change the background image." +msgstr "%s verzoekt de mogelijkheid om uw achtergrondafbeelding te wijzigen." diff --git a/po/oc.po b/po/oc.po new file mode 100644 index 0000000..cb4b15d --- /dev/null +++ b/po/oc.po @@ -0,0 +1,193 @@ +# Occitan translation for xdg-desktop-portal. +# Copyright (C) 2019 xdg-desktop-portal's COPYRIGHT HOLDER +# This file is distributed under the same license as the xdg-desktop-portal package. +# +msgid "" +msgstr "" +"Project-Id-Version: xdg-desktop-portal master\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-04 16:59-0300\n" +"PO-Revision-Date: 2023-02-13 18:49+0100\n" +"Last-Translator: Quentin PAGÈS\n" +"Language-Team: Occitan\n" +"Language: oc\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Poedit 3.2.2\n" + +#: src/background.c:741 +#, c-format +msgid "Allow %s to run in the background?" +msgstr "Permetre a %s de s’executar en rèireplan ?" + +#: src/background.c:745 +#, c-format +msgid "%s requests to be started automatically and run in the background." +msgstr "" +"%s demanda de poder s’aviar automaticament e de s’executar en rèireplan." + +#: src/background.c:747 +#, c-format +msgid "%s requests to run in the background." +msgstr "%s demanda de s’executar en rèireplan." + +#: src/background.c:748 +msgid "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." +msgstr "" +"La permission d’execucion en rèireplan se pòt modificar a tot moment als " +"paramètres d’aplicacion." + +#: src/background.c:753 +msgid "Don't allow" +msgstr "Permetre pas" + +#: src/background.c:754 src/screenshot.c:238 src/wallpaper.c:182 +msgid "Allow" +msgstr "Permetre" + +#: src/device.c:116 +msgid "Turn On Microphone?" +msgstr "Alucar lo microfòn ?" + +#: src/device.c:117 +msgid "" +"Access to your microphone can be changed at any time from the privacy " +"settings." +msgstr "" +"L’accès al microfòn se pòt modificar a tot moment als paramètres de " +"confidencialitat." + +#: src/device.c:121 +msgid "An application wants to use your microphone." +msgstr "Una aplicacion vòl utilizar lo microfòn." + +#: src/device.c:123 +#, c-format +msgid "%s wants to use your microphone." +msgstr "%s vòl utilizar lo microfòn." + +#: src/device.c:129 +msgid "Turn On Speakers?" +msgstr "Alucar los nautparlaires ?" + +#: src/device.c:130 +msgid "" +"Access to your speakers can be changed at any time from the privacy settings." +msgstr "" +"L’accès als nautparlaires se pòt modificar a tot moment als paramètres de " +"confidencialitat." + +#: src/device.c:134 +msgid "An application wants to play sound." +msgstr "Una aplicacion vòl legir de son." + +#: src/device.c:136 +#, c-format +msgid "%s wants to play sound." +msgstr "%s vòl legir de son." + +#: src/device.c:142 +msgid "Turn On Camera?" +msgstr "Alucar la camèra ?" + +#: src/device.c:143 +msgid "" +"Access to your camera can be changed at any time from the privacy settings." +msgstr "" +"L’accès a la camèra se pòt modificar a tot moment als paramètres de " +"confidencialitat." + +#: src/device.c:147 +msgid "An application wants to use your camera." +msgstr "Una aplicacion vòl utilizar la camèra." + +#: src/device.c:149 +#, c-format +msgid "%s wants to use your camera." +msgstr "%s vòl utilizar la camèra." + +#: src/location.c:526 +msgid "Deny Access" +msgstr "Refusar l’accès" + +#: src/location.c:528 +msgid "Grant Access" +msgstr "Acordar l’accès" + +#: src/location.c:534 +msgid "Grant Access to Your Location?" +msgstr "Donar accès a vòstre localizacion ?" + +#: src/location.c:535 +msgid "An application wants to use your location." +msgstr "Una aplicacion vòl utilizar vòstra localizacion." + +#: src/location.c:547 +#, c-format +msgid "Give %s Access to Your Location?" +msgstr "Donar accès a %s a vòstra localizacion ?" + +#: src/location.c:551 +#, c-format +msgid "%s wants to use your location." +msgstr "%s vòl utilizar vòstra localizacion." + +#: src/location.c:554 +msgid "Location access can be changed at any time from the privacy settings." +msgstr "" +"Las règlas d’accès a la localizacion se pòdon a tot moment modificar als " +"paramètres de confidencialitat." + +#: src/screenshot.c:236 src/wallpaper.c:180 +msgid "Deny" +msgstr "Refusar" + +#: src/screenshot.c:252 +#, c-format +msgid "Allow %s to Take Screenshots?" +msgstr "Permetre a %s de prendre de capturas d’ecran ?" + +#: src/screenshot.c:253 +#, c-format +msgid "%s wants to be able to take screenshots at any time." +msgstr "%s vòl poder prendre de capturas d’ecran en tot temps." + +#: src/screenshot.c:261 +msgid "Allow Applications to Take Screenshots?" +msgstr "Permetre a las aplicacions de prendre de capturas d’ecran ?" + +#: src/screenshot.c:262 +msgid "An application wants to be able to take screenshots at any time." +msgstr "Una aplicacion vòl poder prendre de capturas d’ecran en tot temps." + +#: src/screenshot.c:265 src/wallpaper.c:209 +msgid "This permission can be changed at any time from the privacy settings." +msgstr "" +"Las permissions se pòdon modificar a tot moment als paramètres de " +"confidencialitat." + +#: src/settings.c:127 +msgid "Requested setting not found" +msgstr "Paramètre demandat pas trobat" + +#: src/wallpaper.c:192 +msgid "Allow Applications to Set Backgrounds?" +msgstr "Permetre a las aplicacions de definir los fonzes d’ecran ?" + +#: src/wallpaper.c:193 +msgid "An application is requesting to be able to change the background image." +msgstr "Una aplicacion demanda de poder modificar l’imatge de rèireplan." + +#: src/wallpaper.c:205 +#, c-format +msgid "Allow %s to Set Backgrounds?" +msgstr "Permetre a %s de definir de fonzes d’ecran ?" + +#: src/wallpaper.c:206 +#, c-format +msgid "%s is requesting to be able to change the background image." +msgstr "%s demanda de poder modificar l’imatge de rèireplan." diff --git a/po/pl.po b/po/pl.po new file mode 100644 index 0000000..407f65f --- /dev/null +++ b/po/pl.po @@ -0,0 +1,193 @@ +# Polish translation for xdg-desktop-portal. +# Copyright © 2016-2022 the xdg-desktop-portal authors. +# This file is distributed under the same license as the xdg-desktop-portal package. +# Piotr Drąg , 2016-2022. +# Aviary.pl , 2016-2022. +# +msgid "" +msgstr "" +"Project-Id-Version: xdg-desktop-portal\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-04 16:59-0300\n" +"PO-Revision-Date: 2022-09-11 15:05+0200\n" +"Last-Translator: Piotr Drąg \n" +"Language-Team: Polish \n" +"Language: pl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " +"|| n%100>=20) ? 1 : 2);\n" + +#: src/background.c:741 +#, c-format +msgid "Allow %s to run in the background?" +msgstr "Pozwolić programowi „%s” na działanie w tle?" + +#: src/background.c:745 +#, c-format +msgid "%s requests to be started automatically and run in the background." +msgstr "Program „%s” potrzebuje być automatycznie uruchamiany i działać w tle." + +#: src/background.c:747 +#, c-format +msgid "%s requests to run in the background." +msgstr "Program „%s” potrzebuje działać w tle." + +#: src/background.c:748 +msgid "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." +msgstr "" +"Uprawnienie do działania w tle można zmienić w każdej chwili w ustawieniach " +"programów." + +#: src/background.c:753 +msgid "Don't allow" +msgstr "Nie zezwalaj" + +#: src/background.c:754 src/screenshot.c:238 src/wallpaper.c:182 +msgid "Allow" +msgstr "Zezwól" + +#: src/device.c:116 +msgid "Turn On Microphone?" +msgstr "Włączyć mikrofon?" + +#: src/device.c:117 +msgid "" +"Access to your microphone can be changed at any time from the privacy " +"settings." +msgstr "" +"Dostęp do mikrofonu można zmienić w każdej chwili w ustawieniach prywatności." + +#: src/device.c:121 +msgid "An application wants to use your microphone." +msgstr "Program potrzebuje dostępu do mikrofonu." + +#: src/device.c:123 +#, c-format +msgid "%s wants to use your microphone." +msgstr "Program „%s” potrzebuje dostępu do mikrofonu." + +#: src/device.c:129 +msgid "Turn On Speakers?" +msgstr "Włączyć głośniki?" + +#: src/device.c:130 +msgid "" +"Access to your speakers can be changed at any time from the privacy settings." +msgstr "" +"Dostęp do głośników można zmienić w każdej chwili w ustawieniach prywatności." + +#: src/device.c:134 +msgid "An application wants to play sound." +msgstr "Program potrzebuje odtwarzać dźwięk." + +#: src/device.c:136 +#, c-format +msgid "%s wants to play sound." +msgstr "Program „%s” potrzebuje odtwarzać dźwięk." + +#: src/device.c:142 +msgid "Turn On Camera?" +msgstr "Włączyć kamerę?" + +#: src/device.c:143 +msgid "" +"Access to your camera can be changed at any time from the privacy settings." +msgstr "" +"Dostęp do kamery można zmienić w każdej chwili w ustawieniach prywatności." + +#: src/device.c:147 +msgid "An application wants to use your camera." +msgstr "Program potrzebuje dostępu do kamery." + +#: src/device.c:149 +#, c-format +msgid "%s wants to use your camera." +msgstr "Program „%s” potrzebuje dostępu do kamery." + +#: src/location.c:526 +msgid "Deny Access" +msgstr "Odmów dostępu" + +#: src/location.c:528 +msgid "Grant Access" +msgstr "Udziel dostępu" + +#: src/location.c:534 +msgid "Grant Access to Your Location?" +msgstr "Udzielić dostęp do położenia?" + +#: src/location.c:535 +msgid "An application wants to use your location." +msgstr "Program potrzebuje dostępu do położenia." + +#: src/location.c:547 +#, c-format +msgid "Give %s Access to Your Location?" +msgstr "Udzielić dostęp do położenia programowi „%s”?" + +#: src/location.c:551 +#, c-format +msgid "%s wants to use your location." +msgstr "Program „%s” potrzebuje dostępu do położenia." + +#: src/location.c:554 +msgid "Location access can be changed at any time from the privacy settings." +msgstr "" +"Dostęp do położenia można zmienić w każdej chwili w ustawieniach prywatności." + +#: src/screenshot.c:236 src/wallpaper.c:180 +msgid "Deny" +msgstr "Odmów" + +#: src/screenshot.c:252 +#, c-format +msgid "Allow %s to Take Screenshots?" +msgstr "Pozwolić programowi „%s” na wykonywanie zrzutów ekranu?" + +#: src/screenshot.c:253 +#, c-format +msgid "%s wants to be able to take screenshots at any time." +msgstr "" +"Program „%s” potrzebuje mieć możliwość wykonywania zrzutów ekranu w dowolnym " +"czasie." + +#: src/screenshot.c:261 +msgid "Allow Applications to Take Screenshots?" +msgstr "Pozwolić programowi na wykonywanie zrzutów ekranu?" + +#: src/screenshot.c:262 +msgid "An application wants to be able to take screenshots at any time." +msgstr "" +"Program potrzebuje mieć możliwość wykonywania zrzutów ekranu w dowolnym " +"czasie." + +#: src/screenshot.c:265 src/wallpaper.c:209 +msgid "This permission can be changed at any time from the privacy settings." +msgstr "" +"To uprawnienie można zmienić w każdej chwili w ustawieniach prywatności." + +#: src/settings.c:127 +msgid "Requested setting not found" +msgstr "Nie odnaleziono żądanego ustawienia" + +#: src/wallpaper.c:192 +msgid "Allow Applications to Set Backgrounds?" +msgstr "Pozwolić programowi na ustawianie tła?" + +#: src/wallpaper.c:193 +msgid "An application is requesting to be able to change the background image." +msgstr "Program potrzebuje mieć możliwość zmiany obrazu tła." + +#: src/wallpaper.c:205 +#, c-format +msgid "Allow %s to Set Backgrounds?" +msgstr "Pozwolić programowi „%s” na ustawianie tła?" + +#: src/wallpaper.c:206 +#, c-format +msgid "%s is requesting to be able to change the background image." +msgstr "Program „%s” potrzebuje mieć możliwość zmiany obrazu tła." diff --git a/po/pt.po b/po/pt.po new file mode 100644 index 0000000..c40e9f6 --- /dev/null +++ b/po/pt.po @@ -0,0 +1,195 @@ +# Portuguese translation for xdg-desktop-portal. +# Copyright (C) 2022 xdg-desktop-portal's COPYRIGHT HOLDER +# This file is distributed under the same license as the xdg-desktop-portal package. +# Juliano de Souza Camargo , 2022. +# +msgid "" +msgstr "" +"Project-Id-Version: xdg-desktop-portal master\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-04 16:59-0300\n" +"PO-Revision-Date: 2022-01-20 10:40-0300\n" +"Last-Translator: Juliano de Souza Camargo \n" +"Language-Team: Portuguese < https://l10n.gnome.org/teams/pt/>\n" +"Language: pt\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"X-Generator: Gtranslator 40.0\n" + +#: src/background.c:741 +#, c-format +msgid "Allow %s to run in the background?" +msgstr "Permitir a %s correr em segundo plano?" + +#: src/background.c:745 +#, c-format +msgid "%s requests to be started automatically and run in the background." +msgstr "%s requisita iniciar automaticamente e correr em segundo plano." + +#: src/background.c:747 +#, c-format +msgid "%s requests to run in the background." +msgstr "%s requisita correr em segundo plano." + +#: src/background.c:748 +msgid "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." +msgstr "" +"Pode alterar, em qualquer altura, a permissão para “correr em segundo plano” " +"pelas definições da aplicação." + +#: src/background.c:753 +msgid "Don't allow" +msgstr "Não permitir" + +#: src/background.c:754 src/screenshot.c:238 src/wallpaper.c:182 +msgid "Allow" +msgstr "Permitir" + +#: src/device.c:116 +msgid "Turn On Microphone?" +msgstr "Ligar o microfone?" + +#: src/device.c:117 +msgid "" +"Access to your microphone can be changed at any time from the privacy " +"settings." +msgstr "" +"Pode alterar, em qualquer altura, o acesso ao microfone pelas definições de " +"privacidade." + +#: src/device.c:121 +msgid "An application wants to use your microphone." +msgstr "Uma aplicação requisita o uso de seu microfone." + +#: src/device.c:123 +#, c-format +msgid "%s wants to use your microphone." +msgstr "%s requisita o uso de seu microfone." + +#: src/device.c:129 +msgid "Turn On Speakers?" +msgstr "Ligar os altifalantes?" + +#: src/device.c:130 +msgid "" +"Access to your speakers can be changed at any time from the privacy settings." +msgstr "" +"Pode alterar, em qualquer altura, o acesso aos altifalantes pelas definições " +"de privacidade." + +#: src/device.c:134 +msgid "An application wants to play sound." +msgstr "Uma aplicação requisita reproduzir som." + +#: src/device.c:136 +#, c-format +msgid "%s wants to play sound." +msgstr "%s requisita reproduzir som." + +#: src/device.c:142 +msgid "Turn On Camera?" +msgstr "Ligar a câmara?" + +#: src/device.c:143 +msgid "" +"Access to your camera can be changed at any time from the privacy settings." +msgstr "" +"Pode alterar, em qualquer altura, o acesso à câmara pelas definições de " +"privacidade." + +#: src/device.c:147 +msgid "An application wants to use your camera." +msgstr "Uma aplicação requisita o uso de sua câmara." + +#: src/device.c:149 +#, c-format +msgid "%s wants to use your camera." +msgstr "%s requisita o uso de sua câmara." + +#: src/location.c:526 +msgid "Deny Access" +msgstr "Negar acesso" + +#: src/location.c:528 +msgid "Grant Access" +msgstr "Conceder acesso" + +#: src/location.c:534 +msgid "Grant Access to Your Location?" +msgstr "Conceder acesso à sua localização?" + +#: src/location.c:535 +msgid "An application wants to use your location." +msgstr "Uma aplicação requisita acesso à sua localização." + +#: src/location.c:547 +#, c-format +msgid "Give %s Access to Your Location?" +msgstr "Permitir que %s aceda à sua localização?" + +#: src/location.c:551 +#, c-format +msgid "%s wants to use your location." +msgstr "%s quer ter acesso à sua localização." + +#: src/location.c:554 +msgid "Location access can be changed at any time from the privacy settings." +msgstr "" +"Pode alterar, em qualquer altura, o acesso à localização pelas definições de " +"privacidade." + +#: src/screenshot.c:236 src/wallpaper.c:180 +msgid "Deny" +msgstr "Negar" + +#: src/screenshot.c:252 +#, fuzzy, c-format +msgid "Allow %s to Take Screenshots?" +msgstr "Permitir que %s defina o fundo?" + +#: src/screenshot.c:253 +#, c-format +msgid "%s wants to be able to take screenshots at any time." +msgstr "" + +#: src/screenshot.c:261 +#, fuzzy +msgid "Allow Applications to Take Screenshots?" +msgstr "Permitir que aplicações definam o fundo?" + +#: src/screenshot.c:262 +#, fuzzy +msgid "An application wants to be able to take screenshots at any time." +msgstr "Uma aplicação requisita o uso de seu microfone." + +#: src/screenshot.c:265 src/wallpaper.c:209 +msgid "This permission can be changed at any time from the privacy settings." +msgstr "" +"Pode alterar, em qualquer altura, esta permissão pelas definições de " +"privacidade." + +#: src/settings.c:127 +msgid "Requested setting not found" +msgstr "Definição solicitada não encontrada" + +#: src/wallpaper.c:192 +msgid "Allow Applications to Set Backgrounds?" +msgstr "Permitir que aplicações definam o fundo?" + +#: src/wallpaper.c:193 +msgid "An application is requesting to be able to change the background image." +msgstr "Uma aplicação requisita poder alterar a imagem de fundo." + +#: src/wallpaper.c:205 +#, c-format +msgid "Allow %s to Set Backgrounds?" +msgstr "Permitir que %s defina o fundo?" + +#: src/wallpaper.c:206 +#, c-format +msgid "%s is requesting to be able to change the background image." +msgstr "%s requisita poder alterar a imagem de fundo." diff --git a/po/pt_BR.po b/po/pt_BR.po new file mode 100644 index 0000000..9c48332 --- /dev/null +++ b/po/pt_BR.po @@ -0,0 +1,198 @@ +# Brazilian Portuguese translation for xdg-desktop-portal. +# Copyright (C) 2019 xdg-desktop-portal's COPYRIGHT HOLDER +# This file is distributed under the same license as the xdg-desktop-portal package. +# Rafael Fontenelle , 2016-2019. +# +msgid "" +msgstr "" +"Project-Id-Version: xdg-desktop-portal master\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-04 16:59-0300\n" +"PO-Revision-Date: 2019-11-24 22:29-0300\n" +"Last-Translator: Rafael Fontenelle \n" +"Language-Team: Brazilian Portuguese \n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1)\n" +"X-Generator: Gtranslator 3.32.0\n" + +#: src/background.c:741 +#, c-format +msgid "Allow %s to run in the background?" +msgstr "Permitir que %s execute em segundo plano?" + +#: src/background.c:745 +#, c-format +msgid "%s requests to be started automatically and run in the background." +msgstr "%s solicita ser iniciado automaticamente e executado em segundo plano." + +#: src/background.c:747 +#, c-format +msgid "%s requests to run in the background." +msgstr "%s solicita ser executado em segundo plano." + +#: src/background.c:748 +msgid "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." +msgstr "" +"A permissão “executar em segundo plano” pode ser alterada a qualquer tempo a " +"partir das configurações do aplicativo." + +#: src/background.c:753 +msgid "Don't allow" +msgstr "Não permitir" + +#: src/background.c:754 src/screenshot.c:238 src/wallpaper.c:182 +msgid "Allow" +msgstr "Permitir" + +#: src/device.c:116 +msgid "Turn On Microphone?" +msgstr "Ligar o microfone?" + +#: src/device.c:117 +msgid "" +"Access to your microphone can be changed at any time from the privacy " +"settings." +msgstr "" +"Acesso ao seu microfone pode ser alterado a qualquer tempo a partir das " +"configurações de privacidades." + +#: src/device.c:121 +msgid "An application wants to use your microphone." +msgstr "Um aplicativo deseja usar seu microfone." + +#: src/device.c:123 +#, c-format +msgid "%s wants to use your microphone." +msgstr "%s deseja usar seu microfone." + +#: src/device.c:129 +msgid "Turn On Speakers?" +msgstr "Ligar o alto-falante?" + +#: src/device.c:130 +msgid "" +"Access to your speakers can be changed at any time from the privacy settings." +msgstr "" +"Acesso ao seu alto-falante pode ser alterado a qualquer tempo a partir das " +"configurações de privacidades." + +#: src/device.c:134 +msgid "An application wants to play sound." +msgstr "Um aplicativo deseja reproduzir som." + +#: src/device.c:136 +#, c-format +msgid "%s wants to play sound." +msgstr "%s deseja reproduzir som." + +#: src/device.c:142 +msgid "Turn On Camera?" +msgstr "Ligar a câmera?" + +#: src/device.c:143 +msgid "" +"Access to your camera can be changed at any time from the privacy settings." +msgstr "" +"Acesso à sua câmera pode ser alterado a qualquer tempo a partir das " +"configurações de privacidades." + +#: src/device.c:147 +msgid "An application wants to use your camera." +msgstr "Um aplicativo deseja usar sua câmera." + +#: src/device.c:149 +#, c-format +msgid "%s wants to use your camera." +msgstr "%s deseja usar sua câmera." + +#: src/location.c:526 +msgid "Deny Access" +msgstr "Negar acesso" + +#: src/location.c:528 +msgid "Grant Access" +msgstr "Conceder acesso" + +#: src/location.c:534 +msgid "Grant Access to Your Location?" +msgstr "Conceder acesso à sua localização?" + +#: src/location.c:535 +msgid "An application wants to use your location." +msgstr "Um aplicativo deseja usar sua localização." + +#: src/location.c:547 +#, c-format +msgid "Give %s Access to Your Location?" +msgstr "Dar %s acesso a sua localização?" + +#: src/location.c:551 +#, c-format +msgid "%s wants to use your location." +msgstr "%s deseja usar sua localização." + +#: src/location.c:554 +msgid "Location access can be changed at any time from the privacy settings." +msgstr "" +"Acesso à localização pode ser alterado a qualquer tempo a partir das " +"configurações de privacidades." + +#: src/screenshot.c:236 src/wallpaper.c:180 +msgid "Deny" +msgstr "Negar" + +#: src/screenshot.c:252 +#, fuzzy, c-format +msgid "Allow %s to Take Screenshots?" +msgstr "Permitir que %s defina planos de fundo?" + +#: src/screenshot.c:253 +#, c-format +msgid "%s wants to be able to take screenshots at any time." +msgstr "" + +#: src/screenshot.c:261 +#, fuzzy +msgid "Allow Applications to Take Screenshots?" +msgstr "Permitir que aplicativos definam planos de fundo?" + +#: src/screenshot.c:262 +#, fuzzy +msgid "An application wants to be able to take screenshots at any time." +msgstr "Um aplicativo deseja usar seu microfone." + +#: src/screenshot.c:265 src/wallpaper.c:209 +msgid "This permission can be changed at any time from the privacy settings." +msgstr "" +"Essa permissão pode ser alterada a qualquer momento a partir das " +"configurações de privacidades." + +#: src/settings.c:127 +msgid "Requested setting not found" +msgstr "A configuração requisitada não foi encontrada" + +#: src/wallpaper.c:192 +msgid "Allow Applications to Set Backgrounds?" +msgstr "Permitir que aplicativos definam planos de fundo?" + +#: src/wallpaper.c:193 +msgid "An application is requesting to be able to change the background image." +msgstr "" +"Um aplicativo está solicitando a capacidade de alterar a imagem de plano de " +"fundo." + +#: src/wallpaper.c:205 +#, c-format +msgid "Allow %s to Set Backgrounds?" +msgstr "Permitir que %s defina planos de fundo?" + +#: src/wallpaper.c:206 +#, c-format +msgid "%s is requesting to be able to change the background image." +msgstr "" +"%s está solicitando a capacidade de alterar a imagem de plano de fundo." diff --git a/po/ro.po b/po/ro.po new file mode 100644 index 0000000..3236c94 --- /dev/null +++ b/po/ro.po @@ -0,0 +1,195 @@ +# Copyright (C) 2022 xdg-desktop-portal +# This file is distributed under the same license as the xdg-desktop-portal package. +# +# Sergiu Bivol , 2022. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-04 16:59-0300\n" +"PO-Revision-Date: 2022-02-18 10:51+0000\n" +"Last-Translator: Sergiu Bivol \n" +"Language-Team: Romanian\n" +"Language: ro\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < " +"20)) ? 1 : 2;\n" +"X-Generator: Lokalize 21.12.2\n" + +#: src/background.c:741 +#, c-format +msgid "Allow %s to run in the background?" +msgstr "Permiteți %s să ruleze în fundal?" + +#: src/background.c:745 +#, c-format +msgid "%s requests to be started automatically and run in the background." +msgstr "%s cere să fie pornit automat și să ruleze în fundal." + +#: src/background.c:747 +#, c-format +msgid "%s requests to run in the background." +msgstr "%s cere să ruleze în fundal." + +#: src/background.c:748 +msgid "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." +msgstr "" +"Permisiunea „rulează în fundal” poate fi schimbată în orice moment din " +"configurările aplicației." + +#: src/background.c:753 +msgid "Don't allow" +msgstr "Nu permite" + +#: src/background.c:754 src/screenshot.c:238 src/wallpaper.c:182 +msgid "Allow" +msgstr "Permite" + +#: src/device.c:116 +msgid "Turn On Microphone?" +msgstr "Porniți microfonul?" + +#: src/device.c:117 +msgid "" +"Access to your microphone can be changed at any time from the privacy " +"settings." +msgstr "" +"Accesul la microfon poate fi schimbat oricând din configurările de " +"confidențialitate." + +#: src/device.c:121 +msgid "An application wants to use your microphone." +msgstr "O aplicație vrea să vă folosească microfonul." + +#: src/device.c:123 +#, c-format +msgid "%s wants to use your microphone." +msgstr "%s vrea să vă folosească microfonul." + +#: src/device.c:129 +msgid "Turn On Speakers?" +msgstr "Porniți difuzoarele?" + +#: src/device.c:130 +msgid "" +"Access to your speakers can be changed at any time from the privacy settings." +msgstr "" +"Accesul la difuzoare poate fi schimbat oricând din configurările de " +"confidențialitate." + +#: src/device.c:134 +msgid "An application wants to play sound." +msgstr "O aplicație dorește să redea sunet." + +#: src/device.c:136 +#, c-format +msgid "%s wants to play sound." +msgstr "%s dorește să redea sunet." + +#: src/device.c:142 +msgid "Turn On Camera?" +msgstr "Porniți camera?" + +#: src/device.c:143 +msgid "" +"Access to your camera can be changed at any time from the privacy settings." +msgstr "" +"Accesul la cameră poate fi schimbat oricând din configurările de " +"confidențialitate." + +#: src/device.c:147 +msgid "An application wants to use your camera." +msgstr "O aplicație vrea să vă folosească camera." + +#: src/device.c:149 +#, c-format +msgid "%s wants to use your camera." +msgstr "%s vrea să vă folosească camera." + +#: src/location.c:526 +msgid "Deny Access" +msgstr "Refuză accesul" + +#: src/location.c:528 +msgid "Grant Access" +msgstr "Permite accesul" + +#: src/location.c:534 +msgid "Grant Access to Your Location?" +msgstr "Permiteți accesul la localizare?" + +#: src/location.c:535 +msgid "An application wants to use your location." +msgstr "O aplicație vrea să vă folosească localizarea." + +#: src/location.c:547 +#, c-format +msgid "Give %s Access to Your Location?" +msgstr "Permiteți accesul %s la localizare?" + +#: src/location.c:551 +#, c-format +msgid "%s wants to use your location." +msgstr "%s vrea să vă folosească localizarea." + +#: src/location.c:554 +msgid "Location access can be changed at any time from the privacy settings." +msgstr "" +"Accesul la localizare poate fi schimbat oricând din configurările de " +"confidențialitate." + +#: src/screenshot.c:236 src/wallpaper.c:180 +msgid "Deny" +msgstr "Refuză" + +#: src/screenshot.c:252 +#, fuzzy, c-format +msgid "Allow %s to Take Screenshots?" +msgstr "Permiteți %s să stabilească fundaluri?" + +#: src/screenshot.c:253 +#, c-format +msgid "%s wants to be able to take screenshots at any time." +msgstr "" + +#: src/screenshot.c:261 +#, fuzzy +msgid "Allow Applications to Take Screenshots?" +msgstr "Permiteți aplicațiilor să stabilească fundaluri?" + +#: src/screenshot.c:262 +#, fuzzy +msgid "An application wants to be able to take screenshots at any time." +msgstr "O aplicație vrea să vă folosească microfonul." + +#: src/screenshot.c:265 src/wallpaper.c:209 +msgid "This permission can be changed at any time from the privacy settings." +msgstr "" +"Această permisiune poate fi schimbată oricând din configurările de " +"confidențialitate." + +#: src/settings.c:127 +msgid "Requested setting not found" +msgstr "Configurarea cerută nu a fost găsită" + +#: src/wallpaper.c:192 +msgid "Allow Applications to Set Backgrounds?" +msgstr "Permiteți aplicațiilor să stabilească fundaluri?" + +#: src/wallpaper.c:193 +msgid "An application is requesting to be able to change the background image." +msgstr "O aplicație cere să poată schimba imaginea de fundal." + +#: src/wallpaper.c:205 +#, c-format +msgid "Allow %s to Set Backgrounds?" +msgstr "Permiteți %s să stabilească fundaluri?" + +#: src/wallpaper.c:206 +#, c-format +msgid "%s is requesting to be able to change the background image." +msgstr "%s cere să poată schimba imaginea de fundal." diff --git a/po/ru.po b/po/ru.po new file mode 100644 index 0000000..f1839cc --- /dev/null +++ b/po/ru.po @@ -0,0 +1,194 @@ +# Russian translation for xdg-desktop-portal. +# Copyright (C) 2020 xdg-desktop-portal's COPYRIGHT HOLDER +# This file is distributed under the same license as the xdg-desktop-portal package. +# Артемий Судаков , 2020. +# +msgid "" +msgstr "" +"Project-Id-Version: xdg-desktop-portal master\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-04 16:59-0300\n" +"PO-Revision-Date: 2022-10-04 12:51+0300\n" +"Last-Translator: Aleksandr Melman \n" +"Language-Team: Russian \n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Generator: Poedit 3.1.1\n" + +#: src/background.c:741 +#, c-format +msgid "Allow %s to run in the background?" +msgstr "Разрешить %s работать в фоне?" + +#: src/background.c:745 +#, c-format +msgid "%s requests to be started automatically and run in the background." +msgstr "%s запрашивает доступ на автоматический запуск и работу в фоне." + +#: src/background.c:747 +#, c-format +msgid "%s requests to run in the background." +msgstr "%s запрашивает доступ на работу в фоне." + +#: src/background.c:748 +msgid "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." +msgstr "" +"Разрешение на \"работу в фоне\" может быть изменено в любое время из меню " +"настроек приложения." + +#: src/background.c:753 +msgid "Don't allow" +msgstr "Запретить" + +#: src/background.c:754 src/screenshot.c:238 src/wallpaper.c:182 +msgid "Allow" +msgstr "Разрешить" + +#: src/device.c:116 +msgid "Turn On Microphone?" +msgstr "Включить микрофон?" + +#: src/device.c:117 +msgid "" +"Access to your microphone can be changed at any time from the privacy " +"settings." +msgstr "" +"Доступ к микрофону может быть изменен в любое время из меню настроек " +"конфиденциальности." + +#: src/device.c:121 +msgid "An application wants to use your microphone." +msgstr "Приложение запрашивает доступ к микрофону." + +#: src/device.c:123 +#, c-format +msgid "%s wants to use your microphone." +msgstr "%s запрашивает доступ к микрофону." + +#: src/device.c:129 +msgid "Turn On Speakers?" +msgstr "Включить динамики?" + +#: src/device.c:130 +msgid "" +"Access to your speakers can be changed at any time from the privacy settings." +msgstr "" +"Доступ к динамикам может быть изменен в любое время из меню настроек " +"конфиденциальности." + +#: src/device.c:134 +msgid "An application wants to play sound." +msgstr "Приложение хочет воспроизвести звук." + +#: src/device.c:136 +#, c-format +msgid "%s wants to play sound." +msgstr "%s хочет воспроизвести звук." + +#: src/device.c:142 +msgid "Turn On Camera?" +msgstr "Включить камеру?" + +#: src/device.c:143 +msgid "" +"Access to your camera can be changed at any time from the privacy settings." +msgstr "" +"Доступ к камере может быть изменен в любое время из меню настроек " +"конфиденциальности." + +#: src/device.c:147 +msgid "An application wants to use your camera." +msgstr "Приложение запрашивает доступ к камере." + +#: src/device.c:149 +#, c-format +msgid "%s wants to use your camera." +msgstr "%s запрашивает доступ к камере." + +#: src/location.c:526 +msgid "Deny Access" +msgstr "Отклонить запрос" + +#: src/location.c:528 +msgid "Grant Access" +msgstr "Предоставить доступ" + +#: src/location.c:534 +msgid "Grant Access to Your Location?" +msgstr "Предоставить доступ к вашему местоположению?" + +#: src/location.c:535 +msgid "An application wants to use your location." +msgstr "Приложение запрашивает доступ к сервисам геолокации." + +#: src/location.c:547 +#, c-format +msgid "Give %s Access to Your Location?" +msgstr "Дать %s доступ к вашему местоположению?" + +#: src/location.c:551 +#, c-format +msgid "%s wants to use your location." +msgstr "%s запрашивает доступ к использоованию сервисов геолокации." + +#: src/location.c:554 +msgid "Location access can be changed at any time from the privacy settings." +msgstr "" +"Доступ к сервисам местоположения может быть изменен в любое время из меню " +"настроек конфиденциальности." + +#: src/screenshot.c:236 src/wallpaper.c:180 +msgid "Deny" +msgstr "Отклонить" + +#: src/screenshot.c:252 +#, c-format +msgid "Allow %s to Take Screenshots?" +msgstr "Разрешить %s делать снимки экрана?" + +#: src/screenshot.c:253 +#, c-format +msgid "%s wants to be able to take screenshots at any time." +msgstr "%s хочет иметь возможность делать снимки экрана в любое время." + +#: src/screenshot.c:261 +msgid "Allow Applications to Take Screenshots?" +msgstr "Разрешить приложениям делать снимки экрана?" + +#: src/screenshot.c:262 +msgid "An application wants to be able to take screenshots at any time." +msgstr "Приложение хочет иметь возможность делать снимки экрана в любое время." + +#: src/screenshot.c:265 src/wallpaper.c:209 +msgid "This permission can be changed at any time from the privacy settings." +msgstr "" +"Это разрешение может быть изменено в любое время из меню настроек " +"конфиденциальности." + +#: src/settings.c:127 +msgid "Requested setting not found" +msgstr "Запрашиваемая настройка не найдена" + +#: src/wallpaper.c:192 +msgid "Allow Applications to Set Backgrounds?" +msgstr "Разрешить приложениям устанавливать фоновые изображения?" + +#: src/wallpaper.c:193 +msgid "An application is requesting to be able to change the background image." +msgstr "Приложение запрашивает возможность изменения фонового изображения." + +#: src/wallpaper.c:205 +#, c-format +msgid "Allow %s to Set Backgrounds?" +msgstr "Разрешить %s устанавливать фоновые изображения?" + +#: src/wallpaper.c:206 +#, c-format +msgid "%s is requesting to be able to change the background image." +msgstr "%s запрашивает возможность изменения фонового изображения." diff --git a/po/sk.po b/po/sk.po new file mode 100644 index 0000000..10bfa89 --- /dev/null +++ b/po/sk.po @@ -0,0 +1,190 @@ +# Slovak translation for xdg-desktop-portal. +# Copyright (C) 2016 xdg-desktop-portal's COPYRIGHT HOLDER +# This file is distributed under the same license as the xdg-desktop-portal package. +# Dušan Kazik , 2016-2020. +# +msgid "" +msgstr "" +"Project-Id-Version: xdg-desktop-portal master\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-04 16:59-0300\n" +"PO-Revision-Date: 2020-09-22 14:32+0200\n" +"Last-Translator: Dušan Kazik \n" +"Language-Team: Slovak \n" +"Language: sk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 1 : (n>=2 && n<=4) ? 2 : 0;\n" +"X-Generator: Poedit 2.4.1\n" + +#: src/background.c:741 +#, c-format +msgid "Allow %s to run in the background?" +msgstr "Umožniť aplikácii %s spustenie na pozadí?" + +#: src/background.c:745 +#, c-format +msgid "%s requests to be started automatically and run in the background." +msgstr "Aplikácia %s požaduje automatické spustenie a beh na pozadí." + +#: src/background.c:747 +#, c-format +msgid "%s requests to run in the background." +msgstr "Aplikácia %s požaduje spustenie na pozadí." + +#: src/background.c:748 +msgid "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." +msgstr "" +"Oprávnenie „spustenie na pozadí“ môže byť kedykoľvek zmenené z nastavení " +"aplikácií." + +#: src/background.c:753 +msgid "Don't allow" +msgstr "Nepovoliť" + +#: src/background.c:754 src/screenshot.c:238 src/wallpaper.c:182 +msgid "Allow" +msgstr "Povoliť" + +#: src/device.c:116 +msgid "Turn On Microphone?" +msgstr "Zapnúť mikrofón?" + +#: src/device.c:117 +msgid "" +"Access to your microphone can be changed at any time from the privacy " +"settings." +msgstr "" +"Prístup k vášmu mikrofónu môže byť kedykoľvek zmenený z nastavení súkromia." + +#: src/device.c:121 +msgid "An application wants to use your microphone." +msgstr "Aplikácia sa pokúša použiť váš mikrofón." + +#: src/device.c:123 +#, c-format +msgid "%s wants to use your microphone." +msgstr "Aplikácia %s sa pokúša použiť váš mikrofón." + +#: src/device.c:129 +msgid "Turn On Speakers?" +msgstr "Zapnúť reproduktory?" + +#: src/device.c:130 +msgid "" +"Access to your speakers can be changed at any time from the privacy settings." +msgstr "" +"Prístup k vašim reproduktorom môže byť kedykoľvek zmenený z nastavení " +"súkromia." + +#: src/device.c:134 +msgid "An application wants to play sound." +msgstr "Aplikácia sa pokúša prehrať zvuk." + +#: src/device.c:136 +#, c-format +msgid "%s wants to play sound." +msgstr "Aplikácia %s sa pokúša prehrať zvuk." + +#: src/device.c:142 +msgid "Turn On Camera?" +msgstr "Zapnúť kameru?" + +#: src/device.c:143 +msgid "" +"Access to your camera can be changed at any time from the privacy settings." +msgstr "" +"Prístup k vašej kamere môže byť zmenený kedykoľvek z nastavení súkromia." + +#: src/device.c:147 +msgid "An application wants to use your camera." +msgstr "Aplikácia sa pokúša použiť vašu kameru." + +#: src/device.c:149 +#, c-format +msgid "%s wants to use your camera." +msgstr "Aplikácia %s sa pokúša použiť vašu kameru." + +#: src/location.c:526 +msgid "Deny Access" +msgstr "Zamietnuť prístup" + +#: src/location.c:528 +msgid "Grant Access" +msgstr "Udeliť prístup" + +#: src/location.c:534 +msgid "Grant Access to Your Location?" +msgstr "Udeliť prístup k vašej polohe?" + +#: src/location.c:535 +msgid "An application wants to use your location." +msgstr "Aplikácia sa pokúša použiť vašu polohu." + +#: src/location.c:547 +#, c-format +msgid "Give %s Access to Your Location?" +msgstr "Umožniť aplikácii %s prístup k vašej polohe?" + +#: src/location.c:551 +#, c-format +msgid "%s wants to use your location." +msgstr "Aplikácia %s sa pokúša použiť vašu polohu." + +#: src/location.c:554 +msgid "Location access can be changed at any time from the privacy settings." +msgstr "" +"Prístup k vašej polohe môže byť kedykoľvek zmenený z nastavení súkromia." + +#: src/screenshot.c:236 src/wallpaper.c:180 +msgid "Deny" +msgstr "Odmietnuť" + +#: src/screenshot.c:252 +#, fuzzy, c-format +msgid "Allow %s to Take Screenshots?" +msgstr "Umožniť aplikácii %s nastavovať pozadia?" + +#: src/screenshot.c:253 +#, c-format +msgid "%s wants to be able to take screenshots at any time." +msgstr "" + +#: src/screenshot.c:261 +#, fuzzy +msgid "Allow Applications to Take Screenshots?" +msgstr "Umožniť aplikáciám nastavovať pozadia?" + +#: src/screenshot.c:262 +#, fuzzy +msgid "An application wants to be able to take screenshots at any time." +msgstr "Aplikácia sa pokúša použiť váš mikrofón." + +#: src/screenshot.c:265 src/wallpaper.c:209 +msgid "This permission can be changed at any time from the privacy settings." +msgstr "Toto oprávnenie môže byť kedykoľvek zmenené z nastavení súkromia." + +#: src/settings.c:127 +msgid "Requested setting not found" +msgstr "Požadované nastavenie sa nenašlo" + +#: src/wallpaper.c:192 +msgid "Allow Applications to Set Backgrounds?" +msgstr "Umožniť aplikáciám nastavovať pozadia?" + +#: src/wallpaper.c:193 +msgid "An application is requesting to be able to change the background image." +msgstr "Aplikácia požaduje povolenie na zmenu obrázku pozadia." + +#: src/wallpaper.c:205 +#, c-format +msgid "Allow %s to Set Backgrounds?" +msgstr "Umožniť aplikácii %s nastavovať pozadia?" + +#: src/wallpaper.c:206 +#, c-format +msgid "%s is requesting to be able to change the background image." +msgstr "Aplikácia %s požaduje povolenie na zmenu obrázku pozadia." diff --git a/po/sr.po b/po/sr.po new file mode 100644 index 0000000..f21f45b --- /dev/null +++ b/po/sr.po @@ -0,0 +1,200 @@ +# Serbian translation for xdg-desktop-portal. +# Copyright (C) 2016 xdg-desktop-portal's COPYRIGHT HOLDER +# This file is distributed under the same license as the xdg-desktop-portal package. +# MirosNik , 2016. +# Мирослав Николић , 2016. +msgid "" +msgstr "" +"Project-Id-Version: xdg-desktop-portal master\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-04 16:59-0300\n" +"PO-Revision-Date: 2016-09-14 12:54+0200\n" +"Last-Translator: Мирослав Николић \n" +"Language-Team: српски \n" +"Language: sr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=n==1? 3 : n%10==1 && n%100!=11 ? 0 : " +"n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" + +#: src/background.c:741 +#, c-format +msgid "Allow %s to run in the background?" +msgstr "" + +#: src/background.c:745 +#, c-format +msgid "%s requests to be started automatically and run in the background." +msgstr "" + +#: src/background.c:747 +#, c-format +msgid "%s requests to run in the background." +msgstr "" + +#: src/background.c:748 +#, fuzzy +msgid "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." +msgstr "" +"Приступ вашим звучницима се може изменити у било које време из подешавања " +"приватности." + +#: src/background.c:753 +msgid "Don't allow" +msgstr "" + +#: src/background.c:754 src/screenshot.c:238 src/wallpaper.c:182 +msgid "Allow" +msgstr "" + +#: src/device.c:116 +msgid "Turn On Microphone?" +msgstr "Да укључим микрофон?" + +#: src/device.c:117 +msgid "" +"Access to your microphone can be changed at any time from the privacy " +"settings." +msgstr "" +"Приступ вашем микрофону се може изменити у било које време из подешавања " +"приватности." + +#: src/device.c:121 +msgid "An application wants to use your microphone." +msgstr "Један програм жели да користи ваш микрофон." + +#: src/device.c:123 +#, c-format +msgid "%s wants to use your microphone." +msgstr "„%s“ жели да користи ваш микрофон." + +#: src/device.c:129 +msgid "Turn On Speakers?" +msgstr "Да укључим звучнике?" + +#: src/device.c:130 +msgid "" +"Access to your speakers can be changed at any time from the privacy settings." +msgstr "" +"Приступ вашим звучницима се може изменити у било које време из подешавања " +"приватности." + +#: src/device.c:134 +msgid "An application wants to play sound." +msgstr "Један програм жели да пусти звук." + +#: src/device.c:136 +#, c-format +msgid "%s wants to play sound." +msgstr "„%s“ жели да пусти звук." + +#: src/device.c:142 +msgid "Turn On Camera?" +msgstr "Да укључим камерицу?" + +#: src/device.c:143 +msgid "" +"Access to your camera can be changed at any time from the privacy settings." +msgstr "" +"Приступ вашој камерици се може изменити у било које време из подешавања " +"приватности." + +#: src/device.c:147 +msgid "An application wants to use your camera." +msgstr "Један програм жели да користи вашу камерицу." + +#: src/device.c:149 +#, c-format +msgid "%s wants to use your camera." +msgstr "„%s“ жели да користи вашу камерицу." + +#: src/location.c:526 +msgid "Deny Access" +msgstr "" + +#: src/location.c:528 +msgid "Grant Access" +msgstr "" + +#: src/location.c:534 +msgid "Grant Access to Your Location?" +msgstr "" + +#: src/location.c:535 +#, fuzzy +msgid "An application wants to use your location." +msgstr "Један програм жели да користи вашу камерицу." + +#: src/location.c:547 +#, c-format +msgid "Give %s Access to Your Location?" +msgstr "" + +#: src/location.c:551 +#, fuzzy, c-format +msgid "%s wants to use your location." +msgstr "„%s“ жели да користи вашу камерицу." + +#: src/location.c:554 +#, fuzzy +msgid "Location access can be changed at any time from the privacy settings." +msgstr "" +"Приступ вашим звучницима се може изменити у било које време из подешавања " +"приватности." + +#: src/screenshot.c:236 src/wallpaper.c:180 +msgid "Deny" +msgstr "" + +#: src/screenshot.c:252 +#, c-format +msgid "Allow %s to Take Screenshots?" +msgstr "" + +#: src/screenshot.c:253 +#, c-format +msgid "%s wants to be able to take screenshots at any time." +msgstr "" + +#: src/screenshot.c:261 +#, fuzzy +msgid "Allow Applications to Take Screenshots?" +msgstr "Један програм жели да пусти звук." + +#: src/screenshot.c:262 +#, fuzzy +msgid "An application wants to be able to take screenshots at any time." +msgstr "Један програм жели да користи ваш микрофон." + +#: src/screenshot.c:265 src/wallpaper.c:209 +#, fuzzy +msgid "This permission can be changed at any time from the privacy settings." +msgstr "" +"Приступ вашим звучницима се може изменити у било које време из подешавања " +"приватности." + +#: src/settings.c:127 +msgid "Requested setting not found" +msgstr "" + +#: src/wallpaper.c:192 +#, fuzzy +msgid "Allow Applications to Set Backgrounds?" +msgstr "Један програм жели да пусти звук." + +#: src/wallpaper.c:193 +msgid "An application is requesting to be able to change the background image." +msgstr "" + +#: src/wallpaper.c:205 +#, c-format +msgid "Allow %s to Set Backgrounds?" +msgstr "" + +#: src/wallpaper.c:206 +#, c-format +msgid "%s is requesting to be able to change the background image." +msgstr "" diff --git a/po/sv.po b/po/sv.po new file mode 100644 index 0000000..dadca0e --- /dev/null +++ b/po/sv.po @@ -0,0 +1,191 @@ +# Swedish translation for xdg-desktop-portal. +# Copyright © 2016, 2020, 2022 xdg-desktop-portal's COPYRIGHT HOLDER +# This file is distributed under the same license as the xdg-desktop-portal package. +# Sebastian Rasmussen , 2016. +# Anders Jonsson , 2020, 2022. +# +msgid "" +msgstr "" +"Project-Id-Version: xdg-desktop-portal master\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-04 16:59-0300\n" +"PO-Revision-Date: 2022-09-10 15:25+0200\n" +"Last-Translator: Anders Jonsson \n" +"Language-Team: Swedish \n" +"Language: sv\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.1.1\n" + +#: src/background.c:741 +#, c-format +msgid "Allow %s to run in the background?" +msgstr "Tillåt %s att köra i bakgrunden?" + +#: src/background.c:745 +#, c-format +msgid "%s requests to be started automatically and run in the background." +msgstr "%s begär att startas automatiskt och köras i bakgrunden." + +#: src/background.c:747 +#, c-format +msgid "%s requests to run in the background." +msgstr "%s begär att köras i bakgrunden." + +#: src/background.c:748 +msgid "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." +msgstr "" +"Rättigheten ”kör i bakgrund” kan ändras när som helst från " +"programinställningarna." + +#: src/background.c:753 +msgid "Don't allow" +msgstr "Tillåt inte" + +#: src/background.c:754 src/screenshot.c:238 src/wallpaper.c:182 +msgid "Allow" +msgstr "Tillåt" + +#: src/device.c:116 +msgid "Turn On Microphone?" +msgstr "Aktivera mikrofon?" + +#: src/device.c:117 +msgid "" +"Access to your microphone can be changed at any time from the privacy " +"settings." +msgstr "" +"Tillgång till din mikrofon kan ändras när som helst från " +"sekretessinställningarna." + +#: src/device.c:121 +msgid "An application wants to use your microphone." +msgstr "Ett program vill använda din mikrofon." + +#: src/device.c:123 +#, c-format +msgid "%s wants to use your microphone." +msgstr "%s vill använda din mikrofon." + +#: src/device.c:129 +msgid "Turn On Speakers?" +msgstr "Aktivera högtalarna?" + +#: src/device.c:130 +msgid "" +"Access to your speakers can be changed at any time from the privacy settings." +msgstr "" +"Tillgång till dina högtalare kan ändras när som helst från " +"sekretessinställningarna." + +#: src/device.c:134 +msgid "An application wants to play sound." +msgstr "Ett program vill spela upp ljud." + +#: src/device.c:136 +#, c-format +msgid "%s wants to play sound." +msgstr "%s vill spela ett ljud." + +#: src/device.c:142 +msgid "Turn On Camera?" +msgstr "Aktivera kamera?" + +#: src/device.c:143 +msgid "" +"Access to your camera can be changed at any time from the privacy settings." +msgstr "" +"Tillgång till din kamera kan ändras när som helst från " +"sekretessinställningarna." + +#: src/device.c:147 +msgid "An application wants to use your camera." +msgstr "Ett program vill använda din kamera." + +#: src/device.c:149 +#, c-format +msgid "%s wants to use your camera." +msgstr "%s vill använda din kamera." + +#: src/location.c:526 +msgid "Deny Access" +msgstr "Neka åtkomst" + +#: src/location.c:528 +msgid "Grant Access" +msgstr "Bevilja åtkomst" + +#: src/location.c:534 +msgid "Grant Access to Your Location?" +msgstr "Bevilja åtkomst till din plats?" + +#: src/location.c:535 +msgid "An application wants to use your location." +msgstr "Ett program vill använda din plats." + +#: src/location.c:547 +#, c-format +msgid "Give %s Access to Your Location?" +msgstr "Ge %s åtkomst till din plats?" + +#: src/location.c:551 +#, c-format +msgid "%s wants to use your location." +msgstr "%s vill använda din plats." + +#: src/location.c:554 +msgid "Location access can be changed at any time from the privacy settings." +msgstr "Platsåtkomst kan ändras när som helst från sekretessinställningarna." + +#: src/screenshot.c:236 src/wallpaper.c:180 +msgid "Deny" +msgstr "Neka" + +#: src/screenshot.c:252 +#, c-format +msgid "Allow %s to Take Screenshots?" +msgstr "Tillåt %s att ta skärmbilder?" + +#: src/screenshot.c:253 +#, c-format +msgid "%s wants to be able to take screenshots at any time." +msgstr "%s vill kunna ta skärmbilder när som helst." + +#: src/screenshot.c:261 +msgid "Allow Applications to Take Screenshots?" +msgstr "Tillåt program att ta skärmbilder?" + +#: src/screenshot.c:262 +msgid "An application wants to be able to take screenshots at any time." +msgstr "Ett program vill kunna ta skärmbilder när som helst." + +#: src/screenshot.c:265 src/wallpaper.c:209 +msgid "This permission can be changed at any time from the privacy settings." +msgstr "" +"Denna rättighet kan ändras när som helst från sekretessinställningarna." + +#: src/settings.c:127 +msgid "Requested setting not found" +msgstr "Begärd inställning hittades inte" + +#: src/wallpaper.c:192 +msgid "Allow Applications to Set Backgrounds?" +msgstr "Tillåt program att ställa in bakgrunder?" + +#: src/wallpaper.c:193 +msgid "An application is requesting to be able to change the background image." +msgstr "Ett program begär att kunna ändra bakgrundsbilden." + +#: src/wallpaper.c:205 +#, c-format +msgid "Allow %s to Set Backgrounds?" +msgstr "Tillåt %s att ställa in bakgrunder?" + +#: src/wallpaper.c:206 +#, c-format +msgid "%s is requesting to be able to change the background image." +msgstr "%s begär att kunna ändra bakgrundsbilden." diff --git a/po/tr.po b/po/tr.po new file mode 100644 index 0000000..18d141c --- /dev/null +++ b/po/tr.po @@ -0,0 +1,194 @@ +# Turkish translation for xdg-desktop-portal. +# Copyright (C) 2017-2023 xdg-desktop-portal's COPYRIGHT HOLDER +# This file is distributed under the same license as the xdg-desktop-portal package. +# +# Muhammet Kara , 2017. +# Serdar Sağlam , 2019. +# Emin Tufan Çetin , 2019-2020. +# Sabri Ünal , 2020, 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: xdg-desktop-portal master\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-04 16:59-0300\n" +"PO-Revision-Date: 2023-06-06 02:05+0300\n" +"Last-Translator: Berk Elyesa Yıldırım \n" +"Language-Team: Türkçe \n" +"Language: tr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Poedit 3.3.1\n" + +#: src/background.c:741 +#, c-format +msgid "Allow %s to run in the background?" +msgstr "%s, arka planda çalışmasına izin verilsin mi?" + +#: src/background.c:745 +#, c-format +msgid "%s requests to be started automatically and run in the background." +msgstr "%s, kendiliğinden başlamayı ve arka planda çalışmayı istiyor." + +#: src/background.c:747 +#, c-format +msgid "%s requests to run in the background." +msgstr "%s, arka planda çalışmak istiyor." + +#: src/background.c:748 +msgid "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." +msgstr "" +"‘Arka planda çalış’ ayarı, uygulama ayarlarından istediğiniz zaman " +"değiştirilebilir." + +#: src/background.c:753 +msgid "Don't allow" +msgstr "İzin verme" + +#: src/background.c:754 src/screenshot.c:238 src/wallpaper.c:182 +msgid "Allow" +msgstr "İzin ver" + +#: src/device.c:116 +msgid "Turn On Microphone?" +msgstr "Mikrofon Açılsın Mı?" + +#: src/device.c:117 +msgid "" +"Access to your microphone can be changed at any time from the privacy " +"settings." +msgstr "" +"Mikrofonunuza erişimi, istediğiniz zaman gizlilik ayarlarından " +"değiştirebilirsiniz." + +#: src/device.c:121 +msgid "An application wants to use your microphone." +msgstr "Bir uygulama, mikrofonunuzu kullanmak istiyor." + +#: src/device.c:123 +#, c-format +msgid "%s wants to use your microphone." +msgstr "%s, mikrofonunuzu kullanmak istiyor." + +#: src/device.c:129 +msgid "Turn On Speakers?" +msgstr "Hoparlörler Açılsın Mı?" + +#: src/device.c:130 +msgid "" +"Access to your speakers can be changed at any time from the privacy settings." +msgstr "" +"Hoparlörlerinize erişimi, istediğiniz zaman gizlilik ayarlarından " +"değiştirebilirsiniz." + +#: src/device.c:134 +msgid "An application wants to play sound." +msgstr "Bir uygulama, ses çalmak istiyor." + +#: src/device.c:136 +#, c-format +msgid "%s wants to play sound." +msgstr "%s, ses çalmak istiyor." + +#: src/device.c:142 +msgid "Turn On Camera?" +msgstr "Kamera Açılsın Mı?" + +#: src/device.c:143 +msgid "" +"Access to your camera can be changed at any time from the privacy settings." +msgstr "" +"Kameranıza erişimi, istediğiniz zaman gizlilik ayarlarından " +"değiştirebilirsiniz." + +#: src/device.c:147 +msgid "An application wants to use your camera." +msgstr "Bir uygulama, kameranızı kullanmak istiyor." + +#: src/device.c:149 +#, c-format +msgid "%s wants to use your camera." +msgstr "%s, kameranızı kullanmak istiyor." + +#: src/location.c:526 +msgid "Deny Access" +msgstr "Erişimi Reddet" + +#: src/location.c:528 +msgid "Grant Access" +msgstr "Erişim İzni Ver" + +#: src/location.c:534 +msgid "Grant Access to Your Location?" +msgstr "Konumunuza Erişim Verilsin Mi?" + +#: src/location.c:535 +msgid "An application wants to use your location." +msgstr "Bir uygulama, konumunuzu kullanmak istiyor." + +#: src/location.c:547 +#, c-format +msgid "Give %s Access to Your Location?" +msgstr "%s, Bulunduğunuz Konuma Erişsin Mi?" + +#: src/location.c:551 +#, c-format +msgid "%s wants to use your location." +msgstr "%s, konumunuzu kullanmak istiyor." + +#: src/location.c:554 +msgid "Location access can be changed at any time from the privacy settings." +msgstr "" +"Konum erişimini, gizlilik ayarlarından istediğiniz zaman değiştirilebilir." + +#: src/screenshot.c:236 src/wallpaper.c:180 +msgid "Deny" +msgstr "Reddet" + +#: src/screenshot.c:252 +#, c-format +msgid "Allow %s to Take Screenshots?" +msgstr "%s, Ekran Görüntüsü Almasına İzin Verilsin Mi?" + +#: src/screenshot.c:253 +#, c-format +msgid "%s wants to be able to take screenshots at any time." +msgstr "%s istediği zaman ekran görüntüsü alabilmek istiyor." + +#: src/screenshot.c:261 +msgid "Allow Applications to Take Screenshots?" +msgstr "Uygulamaların Ekran Görüntüsü Almasına İzin Verilsin Mi?" + +#: src/screenshot.c:262 +msgid "An application wants to be able to take screenshots at any time." +msgstr "Bir uygulama istediği zaman ekran görüntüsü alabilmek istiyor." + +#: src/screenshot.c:265 src/wallpaper.c:209 +msgid "This permission can be changed at any time from the privacy settings." +msgstr "Bu izin, gizlilik ayarlarından istediğiniz zaman değiştirilebilir." + +#: src/settings.c:127 +msgid "Requested setting not found" +msgstr "İstenen ayar bulunamadı" + +#: src/wallpaper.c:192 +msgid "Allow Applications to Set Backgrounds?" +msgstr "Uygulamaların Arka Planı Belirlemesine İzin Verilsin Mi?" + +#: src/wallpaper.c:193 +msgid "An application is requesting to be able to change the background image." +msgstr "Bir uygulama arka planı değiştirme yetkisi istiyor." + +#: src/wallpaper.c:205 +#, c-format +msgid "Allow %s to Set Backgrounds?" +msgstr "%s, Arka Planı Belirlemesine İzin Verilsin Mi?" + +#: src/wallpaper.c:206 +#, c-format +msgid "%s is requesting to be able to change the background image." +msgstr "%s, arka planı değiştirme yetkisi istiyor." diff --git a/po/uk.po b/po/uk.po new file mode 100644 index 0000000..d0e222b --- /dev/null +++ b/po/uk.po @@ -0,0 +1,196 @@ +# Ukrainian translation for xdg-desktop-portal. +# Copyright (C) 2016 xdg-desktop-portal's COPYRIGHT HOLDER +# This file is distributed under the same license as the xdg-desktop-portal package. +# +# Yuri Chornoivan , 2016, 2018, 2019, 2022. +msgid "" +msgstr "" +"Project-Id-Version: xdg-desktop-portal master\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-04 16:59-0300\n" +"PO-Revision-Date: 2022-10-04 23:10+0300\n" +"Last-Translator: Yuri Chornoivan \n" +"Language-Team: Ukrainian \n" +"Language: uk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=n==1 ? 3 : n%10==1 && n%100!=11 ? 0 : " +"n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"X-Generator: Lokalize 20.12.0\n" + +#: src/background.c:741 +#, c-format +msgid "Allow %s to run in the background?" +msgstr "Дозволити запуск %s у фоновому режимі?" + +#: src/background.c:745 +#, c-format +msgid "%s requests to be started automatically and run in the background." +msgstr "%s надіслано запит щодо автоматичного запуску у фоновому режимі." + +#: src/background.c:747 +#, c-format +msgid "%s requests to run in the background." +msgstr "%s надіслано запит щодо запуску у фоновому режимі." + +#: src/background.c:748 +msgid "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." +msgstr "" +"Параметри запуску у фоновому режимі може бути будь-коли змінено за допомогою " +"налаштовування параметрів програми." + +#: src/background.c:753 +msgid "Don't allow" +msgstr "Не дозволяти" + +#: src/background.c:754 src/screenshot.c:238 src/wallpaper.c:182 +msgid "Allow" +msgstr "Дозволити" + +#: src/device.c:116 +msgid "Turn On Microphone?" +msgstr "Увімкнути мікрофон?" + +#: src/device.c:117 +msgid "" +"Access to your microphone can be changed at any time from the privacy " +"settings." +msgstr "" +"Параметри доступу до мікрофона може бути будь-коли змінено за допомогою " +"налаштувань конфіденційності." + +#: src/device.c:121 +msgid "An application wants to use your microphone." +msgstr "Програмі потрібен доступ до використання мікрофона." + +#: src/device.c:123 +#, c-format +msgid "%s wants to use your microphone." +msgstr "%s потрібен доступ до використання мікрофона." + +#: src/device.c:129 +msgid "Turn On Speakers?" +msgstr "Увімкнути гучномовці?" + +#: src/device.c:130 +msgid "" +"Access to your speakers can be changed at any time from the privacy settings." +msgstr "" +"Параметри доступу до гучномовців може бути будь-коли змінено за допомогою " +"налаштувань конфіденційності." + +#: src/device.c:134 +msgid "An application wants to play sound." +msgstr "Програмі потрібно відтворити звукові дані." + +#: src/device.c:136 +#, c-format +msgid "%s wants to play sound." +msgstr "%s потрібно відтворити звукові дані." + +#: src/device.c:142 +msgid "Turn On Camera?" +msgstr "Увімкнути камеру?" + +#: src/device.c:143 +msgid "" +"Access to your camera can be changed at any time from the privacy settings." +msgstr "" +"Параметри доступу до камери може бути будь-коли змінено за допомогою " +"налаштувань конфіденційності." + +#: src/device.c:147 +msgid "An application wants to use your camera." +msgstr "Програмі потрібен доступ до використання відеокамери." + +#: src/device.c:149 +#, c-format +msgid "%s wants to use your camera." +msgstr "%s потрібен доступ до використання відеокамери." + +#: src/location.c:526 +msgid "Deny Access" +msgstr "Заборонити доступ" + +#: src/location.c:528 +msgid "Grant Access" +msgstr "Надати доступ" + +#: src/location.c:534 +msgid "Grant Access to Your Location?" +msgstr "Надати доступ до даних щодо вашого перебування?" + +#: src/location.c:535 +msgid "An application wants to use your location." +msgstr "" +"Програмі потрібен доступ до використання даних щодо вашого місця перебування." + +#: src/location.c:547 +#, c-format +msgid "Give %s Access to Your Location?" +msgstr "Надати %s доступ до даних щодо вашого перебування?" + +#: src/location.c:551 +#, c-format +msgid "%s wants to use your location." +msgstr "" +"%s потрібен доступ до використання даних щодо вашого місця перебування." + +#: src/location.c:554 +msgid "Location access can be changed at any time from the privacy settings." +msgstr "" +"Параметри доступу до даних щодо вашого місця перебування може бути будь-коли " +"змінено за допомогою налаштувань конфіденційності." + +#: src/screenshot.c:236 src/wallpaper.c:180 +msgid "Deny" +msgstr "Заборонити" + +#: src/screenshot.c:252 +#, c-format +msgid "Allow %s to Take Screenshots?" +msgstr "Дозволити %s робити знімки вікон?" + +#: src/screenshot.c:253 +#, c-format +msgid "%s wants to be able to take screenshots at any time." +msgstr "%s потрібна можливість будь-коли створювати знімки вікон." + +#: src/screenshot.c:261 +msgid "Allow Applications to Take Screenshots?" +msgstr "Дозволити програмам робити знімки вікон?" + +#: src/screenshot.c:262 +msgid "An application wants to be able to take screenshots at any time." +msgstr "Програмі потрібна можливість будь-коли створювати знімки вікон." + +#: src/screenshot.c:265 src/wallpaper.c:209 +msgid "This permission can be changed at any time from the privacy settings." +msgstr "" +"Параметри цього доступу може бути будь-коли змінено за допомогою налаштувань " +"конфіденційності." + +#: src/settings.c:127 +msgid "Requested setting not found" +msgstr "Потрібного вам параметра не знайдено" + +#: src/wallpaper.c:192 +msgid "Allow Applications to Set Backgrounds?" +msgstr "Дозволити програмам встановлювати фонове зображення?" + +#: src/wallpaper.c:193 +msgid "An application is requesting to be able to change the background image." +msgstr "Програмою надіслано запит щодо доступу до зміни фонового зображення." + +#: src/wallpaper.c:205 +#, c-format +msgid "Allow %s to Set Backgrounds?" +msgstr "Дозволити %s встановлювати фонове зображення?" + +#: src/wallpaper.c:206 +#, c-format +msgid "%s is requesting to be able to change the background image." +msgstr "%s надіслано запит щодо доступу до зміни фонового зображення." diff --git a/po/zh_CN.po b/po/zh_CN.po new file mode 100644 index 0000000..83cbe3c --- /dev/null +++ b/po/zh_CN.po @@ -0,0 +1,186 @@ +# Chinese (China) translation for xdg-desktop-portal. +# Copyright (C) 2016 xdg-desktop-portal's COPYRIGHT HOLDER +# This file is distributed under the same license as the xdg-desktop-portal package. +# Mingye Wang , 2016. +# 王滋涵 Zephyr Waitzman , 2019. +# lumingzh , 2020. +# Dingzhong Chen , 2020. +# +msgid "" +msgstr "" +"Project-Id-Version: xdg-desktop-portal master\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-04 16:59-0300\n" +"PO-Revision-Date: 2020-10-09 00:19+0800\n" +"Last-Translator: Dingzhong Chen \n" +"Language-Team: Chinese - China \n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Gtranslator 3.36.0\n" +"Plural-Forms: nplurals=1; plural=0\n" + +#: src/background.c:741 +#, c-format +msgid "Allow %s to run in the background?" +msgstr "允许 %s 在后台运行吗?" + +#: src/background.c:745 +#, c-format +msgid "%s requests to be started automatically and run in the background." +msgstr "%s 请求自动启动并在后台运行。" + +#: src/background.c:747 +#, c-format +msgid "%s requests to run in the background." +msgstr "%s 请求在后台运行。" + +#: src/background.c:748 +msgid "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." +msgstr "您可随时在应用设置中修改“后台运行”的权限。" + +#: src/background.c:753 +msgid "Don't allow" +msgstr "禁止" + +#: src/background.c:754 src/screenshot.c:238 src/wallpaper.c:182 +msgid "Allow" +msgstr "允许" + +#: src/device.c:116 +msgid "Turn On Microphone?" +msgstr "要打开麦克风吗?" + +#: src/device.c:117 +msgid "" +"Access to your microphone can be changed at any time from the privacy " +"settings." +msgstr "您可随时在隐私设置中修改麦克风的访问权限。" + +#: src/device.c:121 +msgid "An application wants to use your microphone." +msgstr "有应用程序想要使用您的麦克风。" + +#: src/device.c:123 +#, c-format +msgid "%s wants to use your microphone." +msgstr "%s 想使用您的麦克风。" + +#: src/device.c:129 +msgid "Turn On Speakers?" +msgstr "要打开扬声器吗?" + +#: src/device.c:130 +msgid "" +"Access to your speakers can be changed at any time from the privacy settings." +msgstr "您可随时在隐私设置中修改扬声器的访问权限。" + +#: src/device.c:134 +msgid "An application wants to play sound." +msgstr "有应用程序想要播放声音。" + +#: src/device.c:136 +#, c-format +msgid "%s wants to play sound." +msgstr "%s 想播放声音。" + +#: src/device.c:142 +msgid "Turn On Camera?" +msgstr "要打开摄像头吗?" + +#: src/device.c:143 +msgid "" +"Access to your camera can be changed at any time from the privacy settings." +msgstr "您可随时在隐私设置中修改摄像头的访问权限。" + +#: src/device.c:147 +msgid "An application wants to use your camera." +msgstr "有应用程序想要使用您的摄像头。" + +#: src/device.c:149 +#, c-format +msgid "%s wants to use your camera." +msgstr "%s 想使用您的摄像头。" + +#: src/location.c:526 +msgid "Deny Access" +msgstr "拒绝访问" + +#: src/location.c:528 +msgid "Grant Access" +msgstr "授权访问" + +#: src/location.c:534 +msgid "Grant Access to Your Location?" +msgstr "授权访问您的位置信息?" + +#: src/location.c:535 +msgid "An application wants to use your location." +msgstr "有应用程序想使用您的位置信息。" + +#: src/location.c:547 +#, c-format +msgid "Give %s Access to Your Location?" +msgstr "授予 %s 访问您的位置信息权限?" + +#: src/location.c:551 +#, c-format +msgid "%s wants to use your location." +msgstr "%s 想使用您的位置信息。" + +#: src/location.c:554 +msgid "Location access can be changed at any time from the privacy settings." +msgstr "您可随时在隐私设置中修改位置信息的访问权限。" + +#: src/screenshot.c:236 src/wallpaper.c:180 +msgid "Deny" +msgstr "拒绝" + +#: src/screenshot.c:252 +#, fuzzy, c-format +msgid "Allow %s to Take Screenshots?" +msgstr "允许 %s 设置背景吗?" + +#: src/screenshot.c:253 +#, c-format +msgid "%s wants to be able to take screenshots at any time." +msgstr "" + +#: src/screenshot.c:261 +#, fuzzy +msgid "Allow Applications to Take Screenshots?" +msgstr "允许应用程序设置背景吗?" + +#: src/screenshot.c:262 +#, fuzzy +msgid "An application wants to be able to take screenshots at any time." +msgstr "有应用程序想要使用您的麦克风。" + +#: src/screenshot.c:265 src/wallpaper.c:209 +msgid "This permission can be changed at any time from the privacy settings." +msgstr "您可随时在隐私设置中修改该权限。" + +#: src/settings.c:127 +msgid "Requested setting not found" +msgstr "未找到请求的设置" + +#: src/wallpaper.c:192 +msgid "Allow Applications to Set Backgrounds?" +msgstr "允许应用程序设置背景吗?" + +#: src/wallpaper.c:193 +msgid "An application is requesting to be able to change the background image." +msgstr "一个应用程序正在请求允许修改背景图像。" + +#: src/wallpaper.c:205 +#, c-format +msgid "Allow %s to Set Backgrounds?" +msgstr "允许 %s 设置背景吗?" + +#: src/wallpaper.c:206 +#, c-format +msgid "%s is requesting to be able to change the background image." +msgstr "%s 正在请求允许修改背景图像。" diff --git a/po/zh_TW.po b/po/zh_TW.po new file mode 100644 index 0000000..2d2501f --- /dev/null +++ b/po/zh_TW.po @@ -0,0 +1,182 @@ +# Chinese (Taiwan) translation for xdg-desktop-portal. +# Copyright (C) 2018 xdg-desktop-portal's COPYRIGHT HOLDER +# This file is distributed under the same license as the xdg-desktop-portal package. +# Cheng-Chia Tseng , 2018. +# +msgid "" +msgstr "" +"Project-Id-Version: xdg-desktop-portal master\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-04 16:59-0300\n" +"PO-Revision-Date: 2022-07-03 04:00+0800\n" +"Last-Translator: Freddy Cheng \n" +"Language-Team: Chinese (Taiwan) \n" +"Language: zh_TW\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 3.0.1\n" + +#: src/background.c:741 +#, c-format +msgid "Allow %s to run in the background?" +msgstr "允許《%s》於背景執行嗎?" + +#: src/background.c:745 +#, c-format +msgid "%s requests to be started automatically and run in the background." +msgstr "《%s》請求自動啟動並於背景執行。" + +#: src/background.c:747 +#, c-format +msgid "%s requests to run in the background." +msgstr "《%s》請求於背景執行。" + +#: src/background.c:748 +msgid "" +"The ‘run in background’ permission can be changed at any time from the " +"application settings." +msgstr "背景執行許可權可隨時從應用程式設定中更改。" + +#: src/background.c:753 +msgid "Don't allow" +msgstr "不允許" + +#: src/background.c:754 src/screenshot.c:238 src/wallpaper.c:182 +msgid "Allow" +msgstr "允許" + +#: src/device.c:116 +msgid "Turn On Microphone?" +msgstr "開啟麥克風?" + +#: src/device.c:117 +msgid "" +"Access to your microphone can be changed at any time from the privacy " +"settings." +msgstr "麥克風取用權可隨時從隱私設定中更改。" + +#: src/device.c:121 +msgid "An application wants to use your microphone." +msgstr "應用程式想要使用您的麥克風。" + +#: src/device.c:123 +#, c-format +msgid "%s wants to use your microphone." +msgstr "《%s》想要使用您的麥克風。" + +#: src/device.c:129 +msgid "Turn On Speakers?" +msgstr "開啟喇叭?" + +#: src/device.c:130 +msgid "" +"Access to your speakers can be changed at any time from the privacy settings." +msgstr "喇叭取用權可隨時從隱私設定中更改。" + +#: src/device.c:134 +msgid "An application wants to play sound." +msgstr "應用程式想要播放聲音。" + +#: src/device.c:136 +#, c-format +msgid "%s wants to play sound." +msgstr "《%s》想要播放聲音。" + +#: src/device.c:142 +msgid "Turn On Camera?" +msgstr "開啟相機?" + +#: src/device.c:143 +msgid "" +"Access to your camera can be changed at any time from the privacy settings." +msgstr "相機取用權可隨時從隱私設定中更改。" + +#: src/device.c:147 +msgid "An application wants to use your camera." +msgstr "應用程式想要使用您的相機。" + +#: src/device.c:149 +#, c-format +msgid "%s wants to use your camera." +msgstr "《%s》想要使用您的相機。" + +#: src/location.c:526 +msgid "Deny Access" +msgstr "不允許" + +#: src/location.c:528 +msgid "Grant Access" +msgstr "允許" + +#: src/location.c:534 +msgid "Grant Access to Your Location?" +msgstr "允許取用您的位置資訊?" + +#: src/location.c:535 +msgid "An application wants to use your location." +msgstr "應用程式想要使用您的位置資訊。" + +#: src/location.c:547 +#, c-format +msgid "Give %s Access to Your Location?" +msgstr "允許《%s》取用您的位置資訊嗎?" + +#: src/location.c:551 +#, c-format +msgid "%s wants to use your location." +msgstr "《%s》想使用您的位置資訊。" + +#: src/location.c:554 +msgid "Location access can be changed at any time from the privacy settings." +msgstr "位置資訊取用權可隨時從隱私設定中更改。" + +#: src/screenshot.c:236 src/wallpaper.c:180 +msgid "Deny" +msgstr "不允許" + +#: src/screenshot.c:252 +#, fuzzy, c-format +msgid "Allow %s to Take Screenshots?" +msgstr "允許《%s》設定背景嗎?" + +#: src/screenshot.c:253 +#, c-format +msgid "%s wants to be able to take screenshots at any time." +msgstr "" + +#: src/screenshot.c:261 +#, fuzzy +msgid "Allow Applications to Take Screenshots?" +msgstr "允許應用程式設定背景?" + +#: src/screenshot.c:262 +#, fuzzy +msgid "An application wants to be able to take screenshots at any time." +msgstr "應用程式想要使用您的麥克風。" + +#: src/screenshot.c:265 src/wallpaper.c:209 +msgid "This permission can be changed at any time from the privacy settings." +msgstr "該許可權可隨時從隱私設定中更改。" + +#: src/settings.c:127 +msgid "Requested setting not found" +msgstr "找不到請求的設定" + +#: src/wallpaper.c:192 +msgid "Allow Applications to Set Backgrounds?" +msgstr "允許應用程式設定背景?" + +#: src/wallpaper.c:193 +msgid "An application is requesting to be able to change the background image." +msgstr "應用程式請求能變更背景圖片的許可權。" + +#: src/wallpaper.c:205 +#, c-format +msgid "Allow %s to Set Backgrounds?" +msgstr "允許《%s》設定背景嗎?" + +#: src/wallpaper.c:206 +#, c-format +msgid "%s is requesting to be able to change the background image." +msgstr "《%s》請求能變更背景圖片的許可權。" diff --git a/src/account.c b/src/account.c new file mode 100644 index 0000000..c404b70 --- /dev/null +++ b/src/account.c @@ -0,0 +1,269 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +#include "config.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include "account.h" +#include "request.h" +#include "documents.h" +#include "xdp-dbus.h" +#include "xdp-impl-dbus.h" +#include "xdp-utils.h" + +typedef struct _Account Account; +typedef struct _AccountClass AccountClass; + +struct _Account +{ + XdpDbusAccountSkeleton parent_instance; +}; + +struct _AccountClass +{ + XdpDbusAccountSkeletonClass parent_class; +}; + +static XdpDbusImplAccount *impl; +static Account *account; + +GType account_get_type (void) G_GNUC_CONST; +static void account_iface_init (XdpDbusAccountIface *iface); + +G_DEFINE_TYPE_WITH_CODE (Account, account, XDP_DBUS_TYPE_ACCOUNT_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_ACCOUNT, + account_iface_init)); + +static void +send_response_in_thread_func (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + Request *request = task_data; + guint response; + GVariant *results; + GVariantBuilder new_results; + g_autoptr(GVariant) idv = NULL; + g_autoptr(GVariant) namev = NULL; + const char *image; + + g_variant_builder_init (&new_results, G_VARIANT_TYPE_VARDICT); + + REQUEST_AUTOLOCK (request); + + response = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (request), "response")); + results = (GVariant *)g_object_get_data (G_OBJECT (request), "results"); + + if (response != 0) + goto out; + + idv = g_variant_lookup_value (results, "id", G_VARIANT_TYPE_STRING); + namev = g_variant_lookup_value (results, "name", G_VARIANT_TYPE_STRING); + + g_variant_builder_add (&new_results, "{sv}", "id", idv); + g_variant_builder_add (&new_results, "{sv}", "name", namev); + + if (g_variant_lookup (results, "image", "&s", &image)) + { + g_autofree char *ruri = NULL; + g_autoptr(GError) error = NULL; + + if (xdp_app_info_is_host (request->app_info)) + ruri = g_strdup (image); + else + ruri = register_document (image, xdp_app_info_get_id (request->app_info), DOCUMENT_FLAG_NONE, &error); + + if (ruri == NULL) + g_warning ("Failed to register %s: %s", image, error->message); + else + { + g_debug ("convert uri '%s' -> '%s'\n", image, ruri); + g_variant_builder_add (&new_results, "{sv}", "image", g_variant_new_string (ruri)); + } + } + +out: + if (request->exported) + { + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + response, + g_variant_builder_end (&new_results)); + request_unexport (request); + } +} + +static void +get_user_information_done (GObject *source, + GAsyncResult *result, + gpointer data) +{ + g_autoptr(Request) request = data; + guint response = 2; + g_autoptr(GVariant) results = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GTask) task = NULL; + + if (!xdp_dbus_impl_account_call_get_user_information_finish (XDP_DBUS_IMPL_ACCOUNT (source), + &response, + &results, + result, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("Backend call failed: %s", error->message); + } + + g_object_set_data (G_OBJECT (request), "response", GINT_TO_POINTER (response)); + if (results) + g_object_set_data_full (G_OBJECT (request), "results", g_variant_ref (results), (GDestroyNotify)g_variant_unref); + + task = g_task_new (NULL, NULL, NULL, NULL); + g_task_set_task_data (task, g_object_ref (request), g_object_unref); + g_task_run_in_thread (task, send_response_in_thread_func); +} + +static gboolean +validate_reason (const char *key, + GVariant *value, + GVariant *options, + GError **error) +{ + const char *string = g_variant_get_string (value, NULL); + + if (g_utf8_strlen (string, -1) > 256) + { + g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Not accepting overly long reasons"); + return FALSE; + } + + return TRUE; +} + +static XdpOptionKey user_information_options[] = { + { "reason", G_VARIANT_TYPE_STRING, validate_reason }, +}; + +static gboolean +handle_get_user_information (XdpDbusAccount *object, + GDBusMethodInvocation *invocation, + const gchar *arg_parent_window, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + const char *app_id = xdp_app_info_get_id (request->app_info); + g_autoptr(GError) error = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + GVariantBuilder options; + + g_debug ("Handling GetUserInformation"); + + REQUEST_AUTOLOCK (request); + + impl_request = xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, &error); + if (!impl_request) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request (request, impl_request); + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT); + xdp_filter_options (arg_options, &options, + user_information_options, G_N_ELEMENTS (user_information_options), + NULL); + + g_debug ("options filtered"); + + xdp_dbus_impl_account_call_get_user_information (impl, + request->id, + app_id, + arg_parent_window, + g_variant_builder_end (&options), + NULL, + get_user_information_done, + g_object_ref (request)); + + xdp_dbus_account_complete_get_user_information (object, invocation, + request->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +account_iface_init (XdpDbusAccountIface *iface) +{ + iface->handle_get_user_information = handle_get_user_information; +} + +static void +account_init (Account *account) +{ + xdp_dbus_account_set_version (XDP_DBUS_ACCOUNT (account), 1); +} + +static void +account_class_init (AccountClass *klass) +{ +} + +GDBusInterfaceSkeleton * +account_create (GDBusConnection *connection, + const char *dbus_name) +{ + g_autoptr(GError) error = NULL; + + impl = xdp_dbus_impl_account_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_NONE, + dbus_name, + DESKTOP_PORTAL_OBJECT_PATH, + NULL, + &error); + + if (impl == NULL) + { + g_warning ("Failed to create account proxy: %s", error->message); + return NULL; + } + + g_debug ("using %s at %s\n", "org.freedesktop.impl.portal.Account", dbus_name); + g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (impl), G_MAXINT); + + account = g_object_new (account_get_type (), NULL); + + return G_DBUS_INTERFACE_SKELETON (account); +} diff --git a/src/account.h b/src/account.h new file mode 100644 index 0000000..6e466f2 --- /dev/null +++ b/src/account.h @@ -0,0 +1,26 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +#pragma once + +#include + +GDBusInterfaceSkeleton * account_create (GDBusConnection *connection, + const char *dbus_name); diff --git a/src/background-monitor.c b/src/background-monitor.c new file mode 100644 index 0000000..687c27f --- /dev/null +++ b/src/background-monitor.c @@ -0,0 +1,165 @@ +/* background-monitor.c + * + * Copyright 2022 Georges Basile Stavracas Neto + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "background-monitor.h" + +#define BACKGROUND_MONITOR_BUS_NAME "org.freedesktop.background.Monitor" +#define BACKGROUND_MONITOR_OBJECT_PATH "/org/freedesktop/background/monitor" + +struct _BackgroundMonitor +{ + XdpDbusBackgroundMonitorSkeleton parent_instance; + + GDBusConnection *connection; +}; + +static void g_initable_iface_init (GInitableIface *iface); + +G_DEFINE_TYPE_WITH_CODE (BackgroundMonitor, + background_monitor, + XDP_DBUS_BACKGROUND_TYPE_MONITOR_SKELETON, + G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, g_initable_iface_init)) + +static gboolean +request_freedesktop_background_name (BackgroundMonitor *self, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GVariant) reply = NULL; + GBusNameOwnerFlags flags; + guint32 result; + + flags = G_BUS_NAME_OWNER_FLAGS_REPLACE; +#if GLIB_CHECK_VERSION(2,54,0) + flags |= G_BUS_NAME_OWNER_FLAGS_DO_NOT_QUEUE; +#endif + + reply = g_dbus_connection_call_sync (self->connection, + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + "org.freedesktop.DBus", + "RequestName", + g_variant_new ("(su)", BACKGROUND_MONITOR_BUS_NAME, flags), + G_VARIANT_TYPE ("(u)"), + 0, -1, + cancellable, + error); + + if (!reply) + return FALSE; + + g_variant_get (reply, "(u)", &result); + if (result != 1) + { + g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "Failed to own background monitor D-Bus name"); + return FALSE; + } + + return TRUE; +} + +static gboolean +background_monitor_initable_init (GInitable *initable, + GCancellable *cancellable, + GError **error) +{ + BackgroundMonitor *self = BACKGROUND_MONITOR (initable); + g_autofree char *address = NULL; + + address = g_dbus_address_get_for_bus_sync (G_BUS_TYPE_SESSION, cancellable, error); + if (!address) + return FALSE; + + self->connection = g_initable_new (G_TYPE_DBUS_CONNECTION, + cancellable, error, + "address", address, + "flags", G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT | +#if GLIB_CHECK_VERSION(2,74,0) + G_DBUS_CONNECTION_FLAGS_CROSS_NAMESPACE | +#endif + G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION, + "exit-on-close", TRUE, + NULL); + + if (!self->connection) + return FALSE; + + g_dbus_interface_skeleton_set_flags (G_DBUS_INTERFACE_SKELETON (initable), + G_DBUS_INTERFACE_SKELETON_FLAGS_HANDLE_METHOD_INVOCATIONS_IN_THREAD); + + /* TODO: dos it need to listen to 'g-authorize-method'? */ + + if (!g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (initable), + self->connection, + BACKGROUND_MONITOR_OBJECT_PATH, + error)) + { + return FALSE; + } + + if (!request_freedesktop_background_name (self, cancellable, error)) + return FALSE; + + return TRUE; +} + +static void +g_initable_iface_init (GInitableIface *iface) +{ + iface->init = background_monitor_initable_init; +} + +static void +background_monitor_finalize (GObject *object) +{ + BackgroundMonitor *self = (BackgroundMonitor *)object; + + if (self->connection) + g_dbus_connection_flush_sync (self->connection, NULL, NULL); + + g_clear_object (&self->connection); + + G_OBJECT_CLASS (background_monitor_parent_class)->finalize (object); +} + +static void +background_monitor_class_init (BackgroundMonitorClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = background_monitor_finalize; +} + +static void +background_monitor_init (BackgroundMonitor *self) +{ + xdp_dbus_background_monitor_set_version (XDP_DBUS_BACKGROUND_MONITOR (self), 1); +} + +BackgroundMonitor * +background_monitor_new (GCancellable *cancellable, + GError **error) +{ + return g_initable_new (BACKGROUND_TYPE_MONITOR, + cancellable, + error, + NULL); +} diff --git a/src/background-monitor.h b/src/background-monitor.h new file mode 100644 index 0000000..f407183 --- /dev/null +++ b/src/background-monitor.h @@ -0,0 +1,38 @@ +/* background-monitor.h + * + * Copyright 2022 Georges Basile Stavracas Neto + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#include + +#include "xdp-background-dbus.h" + +G_BEGIN_DECLS + +#define BACKGROUND_TYPE_MONITOR (background_monitor_get_type()) +G_DECLARE_FINAL_TYPE (BackgroundMonitor, + background_monitor, + BACKGROUND, MONITOR, + XdpDbusBackgroundMonitorSkeleton) + +BackgroundMonitor *background_monitor_new (GCancellable *cancellable, + GError **error); + +G_END_DECLS diff --git a/src/background.c b/src/background.c new file mode 100644 index 0000000..b09ad47 --- /dev/null +++ b/src/background.c @@ -0,0 +1,1206 @@ +/* + * Copyright © 2019 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +#include "config.h" + +#include +#include +#include +#include + +#include "call.h" +#include "background.h" +#include "background-monitor.h" +#include "request.h" +#include "permissions.h" +#include "xdp-dbus.h" +#include "xdp-impl-dbus.h" +#include "xdp-utils.h" +#include "flatpak-instance.h" + +/* Implementation notes: + * + * We store a YES/NO/ASK permission for "run in background". + * + * There is a portal api for apps to request this permission + * ahead of time. The portal also lets apps ask for being + * autostarted. + * + * We determine this condition by getting per-application + * state from the compositor, and comparing that list to + * the list of running flatpak instances obtained from + * $XDG_RUNTIME_DIR/.flatpak/. A thread is comparing + * this list every minute, and if it finds an app that + * is in the background twice, we take actions: + * - if the permission is NO, we kill it + * - if the permission is YES or ASK, we notify the user + * + * We only notify once per running instance to not be + * annoying. + * + * Platform-dependent parts are in the background portal + * backend: + * - Notifying the user + * - Getting compositor state + * - Enable or disable autostart + */ + +#define PERMISSION_TABLE "background" +#define PERMISSION_ID "background" + +typedef struct _Background Background; +typedef struct _BackgroundClass BackgroundClass; + +struct _Background +{ + XdpDbusBackgroundSkeleton parent_instance; + + BackgroundMonitor *monitor; +}; + +struct _BackgroundClass +{ + XdpDbusBackgroundSkeletonClass parent_class; +}; + +static XdpDbusImplAccess *access_impl; +static XdpDbusImplBackground *background_impl; +static Background *background; +static GFileMonitor *instance_monitor; + +GType background_get_type (void) G_GNUC_CONST; +static void background_iface_init (XdpDbusBackgroundIface *iface); + +G_DEFINE_TYPE_WITH_CODE (Background, background, + XDP_DBUS_TYPE_BACKGROUND_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_BACKGROUND, + background_iface_init)); + +typedef enum { + BACKGROUND = 0, + RUNNING = 1, + ACTIVE = 2, +} AppState; + +typedef enum { + FORBID = 0, + ALLOW = 1, + IGNORE = 2 +} NotifyResult; + +typedef enum { + AUTOSTART_FLAGS_NONE = 0, + AUTOSTART_FLAGS_ACTIVATABLE = 1 << 0, +} AutostartFlags; + +static GVariant * +get_all_permissions (void) +{ + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) out_perms = NULL; + g_autoptr(GVariant) out_data = NULL; + + if (!xdp_dbus_impl_permission_store_call_lookup_sync (get_permission_store (), + PERMISSION_TABLE, + PERMISSION_ID, + &out_perms, + &out_data, + NULL, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_debug ("No background permissions found: %s", error->message); + return NULL; + } + + return g_steal_pointer (&out_perms); +} + +static Permission +get_one_permission (const char *app_id, + GVariant *perms) +{ + const char **permissions; + + if (perms == NULL) + { + g_debug ("No background permissions found"); + + return PERMISSION_UNSET; + } + else if (!g_variant_lookup (perms, app_id, "^a&s", &permissions)) + { + g_debug ("No background permissions stored for: app %s", app_id); + + return PERMISSION_UNSET; + } + else if (g_strv_length ((char **)permissions) != 1) + { + g_autofree char *a = g_strjoinv (" ", (char **)permissions); + g_warning ("Wrong background permission format, ignoring (%s)", a); + return PERMISSION_UNSET; + } + + g_debug ("permission store: background, app %s -> %s", app_id, permissions[0]); + + if (strcmp (permissions[0], "yes") == 0) + return PERMISSION_YES; + else if (strcmp (permissions[0], "no") == 0) + return PERMISSION_NO; + else if (strcmp (permissions[0], "ask") == 0) + return PERMISSION_ASK; + else + { + g_autofree char *a = g_strjoinv (" ", (char **)permissions); + g_warning ("Wrong permission format, ignoring (%s)", a); + } + + return PERMISSION_UNSET; +} + +static Permission +get_permission (const char *app_id) +{ + g_autoptr(GVariant) perms = NULL; + + perms = get_all_permissions (); + if (perms) + return get_one_permission (app_id, perms); + + return PERMISSION_UNSET; +} + +static void +set_permission (const char *app_id, + Permission permission) +{ + g_autoptr(GError) error = NULL; + const char *permissions[2]; + + if (permission == PERMISSION_ASK) + permissions[0] = "ask"; + else if (permission == PERMISSION_YES) + permissions[0] = "yes"; + else if (permission == PERMISSION_NO) + permissions[0] = "no"; + else + { + g_warning ("Wrong permission format, ignoring"); + return; + } + permissions[1] = NULL; + + if (!xdp_dbus_impl_permission_store_call_set_permission_sync (get_permission_store (), + PERMISSION_TABLE, + TRUE, + PERMISSION_ID, + app_id, + (const char * const*)permissions, + NULL, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("Error updating permission store: %s", error->message); + } +} + +/* background monitor */ + +/* The background monitor is running in a dedicated thread. + * + * We rely on the RunningApplicationsChanged signal from the backend to get + * notified about applications that start or stop having open windows, and on + * file monitoring to learn about flatpak instances appearing and disappearing. + * + * When either of these changes happens, we wake up the background monitor + * thread, and it will check the state of applications a few times, with a + * few seconds of wait in between. When we find an application in the background + * more than once, we check the permissions, and kill or notify if warranted. + * + * We require an application to be in background state for more than once check + * to avoid killing an unlucky application that just happened to start up as we + * did our check. + */ + +static GHashTable * +get_app_states (void) +{ + g_autoptr(GVariant) apps = NULL; + g_autoptr(GHashTable) app_states = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + const char *appid; + GVariant *value; + g_autoptr(GError) error = NULL; + + if (!xdp_dbus_impl_background_call_get_app_state_sync (background_impl, &apps, NULL, &error)) + { + static int warned = 0; + + if (!warned) + { + g_warning ("Failed to get application states: %s", error->message); + warned = 1; + } + + return NULL; + } + + g_autoptr(GVariantIter) iter = g_variant_iter_new (apps); + while (g_variant_iter_loop (iter, "{&sv}", &appid, &value)) + { + AppState state = g_variant_get_uint32 (value); + g_hash_table_insert (app_states, g_strdup (appid), GINT_TO_POINTER (state)); + } + + return g_steal_pointer (&app_states); +} + +static AppState +get_one_app_state (const char *app_id, + GHashTable *app_states) +{ + return (AppState)GPOINTER_TO_INT (g_hash_table_lookup (app_states, app_id)); +} + +typedef struct { + FlatpakInstance *instance; + int stamp; + AppState state; + char *handle; + gboolean notified; + Permission permission; + char *status_message; +} InstanceData; + +static void +instance_data_free (gpointer data) +{ + InstanceData *idata = data; + + g_object_unref (idata->instance); + g_free (idata->status_message); + g_free (idata->handle); + + g_free (idata); +} + +/* instance ID -> InstanceData + */ +static GHashTable *applications; +G_LOCK_DEFINE (applications); + +static void +close_notification (const char *handle) +{ + g_dbus_connection_call (g_dbus_proxy_get_connection (G_DBUS_PROXY (background_impl)), + g_dbus_proxy_get_name (G_DBUS_PROXY (background_impl)), + handle, + "org.freedesktop.impl.portal.Request", + "Close", + NULL, + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, NULL, NULL); +} + +static void +remove_outdated_instances (int stamp) +{ + GHashTableIter iter; + char *id; + InstanceData *data; + g_autoptr(GPtrArray) handles = NULL; + int i; + + handles = g_ptr_array_new_with_free_func (g_free); + + G_LOCK (applications); + g_hash_table_iter_init (&iter, applications); + while (g_hash_table_iter_next (&iter, (gpointer *)&id, (gpointer *)&data)) + { + if (data->stamp < stamp) + { + if (data->handle) + g_ptr_array_add (handles, g_strdup (data->handle)); + g_hash_table_iter_remove (&iter); + } + } + G_UNLOCK (applications); + + for (i = 0; i < handles->len; i++) + { + char *handle = g_ptr_array_index (handles, i); + close_notification (handle); + } +} + +static void +update_background_monitor_properties (void) +{ + GVariantBuilder builder; + GHashTableIter iter; + InstanceData *data; + char *id; + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("aa{sv}")); + + G_LOCK (applications); + g_hash_table_iter_init (&iter, applications); + while (g_hash_table_iter_next (&iter, (gpointer *)&id, (gpointer *)&data)) + { + GVariantBuilder app_builder; + const char *app_id; + const char *id; + + if (data->state != BACKGROUND) + continue; + + if (!flatpak_instance_is_running (data->instance)) + continue; + + id = flatpak_instance_get_id (data->instance); + app_id = flatpak_instance_get_app (data->instance); + g_assert (app_id != NULL); + + g_variant_builder_init (&app_builder, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add (&app_builder, "{sv}", "app_id", g_variant_new_string (app_id)); + g_variant_builder_add (&app_builder, "{sv}", "instance", g_variant_new_string (id)); + if (data->status_message) + g_variant_builder_add (&app_builder, "{sv}", "message", g_variant_new_string (data->status_message)); + + g_variant_builder_add_value (&builder, g_variant_builder_end (&app_builder)); + } + G_UNLOCK (applications); + + xdp_dbus_background_monitor_set_background_apps (XDP_DBUS_BACKGROUND_MONITOR (background->monitor), + g_variant_builder_end (&builder)); +} + +static char * +flatpak_instance_get_display_name (FlatpakInstance *instance) +{ + const char *app_id = flatpak_instance_get_app (instance); + if (app_id[0] != 0) + { + g_autofree char *desktop_id = NULL; + g_autoptr(GAppInfo) info = NULL; + + desktop_id = g_strconcat (app_id, ".desktop", NULL); + info = (GAppInfo*)g_desktop_app_info_new (desktop_id); + + if (info) + return g_strdup (g_app_info_get_display_name (info)); + } + + return g_strdup (app_id); +} + +typedef struct { + char *handle; + char *app_id; + char *id; + char *name; + Permission perm; + pid_t child_pid; +} NotificationData; + +static void +notification_data_free (gpointer data) +{ + NotificationData *nd = data; + + g_free (nd->handle); + g_free (nd->app_id); + g_free (nd->id); + g_free (nd->name); + g_free (nd); +} + +static void +notify_background_done (GObject *source, + GAsyncResult *res, + gpointer data) +{ + NotificationData *nd = (NotificationData *)data; + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) results = NULL; + guint response; + guint result; + InstanceData *idata; + + if (!xdp_dbus_impl_background_call_notify_background_finish (background_impl, + &response, + &results, + res, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("Error from background backend: %s", error->message); + notification_data_free (nd); + return; + } + + if (!g_variant_lookup (results, "result", "u", &result)) + result = IGNORE; + + if (result == ALLOW) + { + g_debug ("Allowing app %s to run in background", nd->app_id); + + if (nd->perm != PERMISSION_ASK) + nd->perm = PERMISSION_YES; + } + else if (result == FORBID) + { + g_debug ("Forbid app %s to run in background", nd->app_id); + + if (nd->perm != PERMISSION_ASK) + nd->perm = PERMISSION_NO; + + g_debug ("Kill app %s (pid %d)", nd->app_id, nd->child_pid); + + kill (nd->child_pid, SIGKILL); + } + else if (result == IGNORE) + { + g_debug ("Allow this instance of %s to run in background without permission changes", nd->app_id); + } + else + g_debug ("Unexpected response from NotifyBackground: %u", result); + + if (nd->perm != PERMISSION_UNSET) + set_permission (nd->app_id, nd->perm); + + G_LOCK (applications); + idata = g_hash_table_lookup (applications, nd->id); + if (idata) + { + g_clear_pointer (&idata->handle, g_free); + idata->permission = nd->perm; + } + G_UNLOCK (applications); + + notification_data_free (nd); +} + +static void +check_background_apps (void) +{ + g_autoptr(GVariant) perms = NULL; + g_autoptr(GHashTable) app_states = NULL; + g_autoptr(GPtrArray) instances = NULL; + int i; + static int stamp; + g_autoptr(GPtrArray) notifications = NULL; + + app_states = get_app_states (); + if (app_states == NULL) + return; + + g_debug ("Checking background permissions"); + + perms = get_all_permissions (); + instances = flatpak_instance_get_all (); + notifications = g_ptr_array_new (); + + stamp++; + + G_LOCK (applications); + for (i = 0; i < instances->len; i++) + { + FlatpakInstance *instance = g_ptr_array_index (instances, i); + const char *id; + const char *app_id; + pid_t child_pid; + InstanceData *idata; + const char *state_names[] = { "background", "running", "active" }; + gboolean is_new = FALSE; + + if (!flatpak_instance_is_running (instance)) + continue; + + id = flatpak_instance_get_id (instance); + app_id = flatpak_instance_get_app (instance); + child_pid = flatpak_instance_get_child_pid (instance); + + idata = g_hash_table_lookup (applications, id); + + if (!app_id) + continue; + + if (!idata) + { + is_new = TRUE; + idata = g_new0 (InstanceData, 1); + idata->instance = g_object_ref (instance); + g_hash_table_insert (applications, g_strdup (id), idata); + } + + idata->stamp = stamp; + idata->state = get_one_app_state (app_id, app_states); + + g_debug ("App %s is %s", app_id, state_names[idata->state]); + + idata->permission = get_one_permission (app_id, perms); + + /* If the app is not in the list yet, add it, + * but don't notify yet - this gives apps some + * leeway to get their window up. If it is still + * in the background next time around, we'll proceed + * to the next step. + */ + if (idata->state != BACKGROUND || idata->notified || is_new) + { + if (idata->notified) + g_debug ("Already notified app %s ...skipping\n", app_id); + if (is_new) + g_debug ("App %s is new ...skipping\n", app_id); + continue; + } + + switch (idata->permission) + { + case PERMISSION_NO: + idata->stamp = 0; + + g_debug ("Kill app %s (pid %d)", app_id, child_pid); + kill (child_pid, SIGKILL); + break; + + case PERMISSION_ASK: + case PERMISSION_UNSET: + { + NotificationData *nd = g_new0 (NotificationData, 1); + + if (idata->handle) + { + close_notification (idata->handle); + g_free (idata->handle); + } + + idata->handle = g_strdup_printf ("/org/freedesktop/portal/desktop/notify/background%d", stamp); + idata->notified = TRUE; + + nd->handle = g_strdup (idata->handle); + nd->name = flatpak_instance_get_display_name (instance); + nd->app_id = g_strdup (app_id); + nd->id = g_strdup (id); + nd->child_pid = child_pid; + nd->perm = idata->permission; + + g_ptr_array_add (notifications, nd); + } + break; + + case PERMISSION_YES: + default: + break; + } + } + G_UNLOCK (applications); + + for (i = 0; i < notifications->len; i++) + { + NotificationData *nd = g_ptr_array_index (notifications, i); + + g_debug ("Notify background for %s", nd->app_id); + + xdp_dbus_impl_background_call_notify_background (background_impl, + nd->handle, + nd->app_id, + nd->name, + NULL, + notify_background_done, + nd); + } + + remove_outdated_instances (stamp); + update_background_monitor_properties (); +} + +static GMainContext *monitor_context; + +static gpointer +background_monitor (gpointer data) +{ + applications = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, instance_data_free); + + while (TRUE) + { + g_main_context_iteration (monitor_context, TRUE); + /* We check twice, to avoid killing unlucky apps hit at a bad time */ + sleep (5); + check_background_apps (); + sleep (5); + check_background_apps (); + } + + g_clear_pointer (&applications, g_hash_table_unref); + g_clear_pointer (&monitor_context, g_main_context_unref); + + return NULL; +} + +static void +start_background_monitor (void) +{ + g_autoptr(GThread) thread = NULL; + + g_debug ("Starting background app monitor"); + + monitor_context = g_main_context_new (); + + thread = g_thread_new ("background monitor", background_monitor, NULL); +} + +static void +running_apps_changed (gpointer data) +{ + g_debug ("Running app windows changed, wake up monitor thread"); + g_main_context_wakeup (monitor_context); +} + +static void +instances_changed (gpointer data) +{ + g_debug ("Running instances changed, wake up monitor thread"); + g_main_context_wakeup (monitor_context); +} + +static void +handle_request_background_in_thread_func (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + Request *request = (Request *)task_data; + GVariant *options; + const char *app_id; + Permission permission; + const char *reason = NULL; + gboolean autostart_requested = FALSE; + gboolean autostart_enabled; + gboolean allowed; + g_autoptr(GError) error = NULL; + const char * const *autostart_exec = { NULL }; + AutostartFlags autostart_flags = AUTOSTART_FLAGS_NONE; + gboolean activatable = FALSE; + g_auto(GStrv) commandline = NULL; + + REQUEST_AUTOLOCK (request); + + options = (GVariant *)g_object_get_data (G_OBJECT (request), "options"); + g_variant_lookup (options, "reason", "&s", &reason); + g_variant_lookup (options, "autostart", "b", &autostart_requested); + g_variant_lookup (options, "commandline", "^a&s", &autostart_exec); + g_variant_lookup (options, "dbus-activatable", "b", &activatable); + + if (activatable) + autostart_flags |= AUTOSTART_FLAGS_ACTIVATABLE; + + app_id = xdp_app_info_get_id (request->app_info); + + if (xdp_app_info_is_host (request->app_info)) + permission = PERMISSION_YES; + else + permission = get_permission (app_id); + + g_debug ("Handle RequestBackground for '%s'", app_id); + + if (permission == PERMISSION_ASK) + { + GVariantBuilder opt_builder; + g_autofree char *title = NULL; + g_autofree char *subtitle = NULL; + g_autofree char *body = NULL; + guint32 response = 2; + g_autoptr(GVariant) results = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GAppInfo) info = NULL; + + info = xdp_app_info_load_app_info (request->app_info); + + title = g_strdup_printf (_("Allow %s to run in the background?"), info ? g_app_info_get_display_name (info) : app_id); + if (reason) + subtitle = g_strdup (reason); + else if (autostart_requested) + subtitle = g_strdup_printf (_("%s requests to be started automatically and run in the background."), info ? g_app_info_get_display_name (info) : app_id); + else + subtitle = g_strdup_printf (_("%s requests to run in the background."), info ? g_app_info_get_display_name (info) : app_id); + body = g_strdup (_("The ‘run in background’ permission can be changed at any time from the application settings.")); + + g_debug ("Calling backend for background access for: %s", app_id); + + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add (&opt_builder, "{sv}", "deny_label", g_variant_new_string (_("Don't allow"))); + g_variant_builder_add (&opt_builder, "{sv}", "grant_label", g_variant_new_string (_("Allow"))); + if (!xdp_dbus_impl_access_call_access_dialog_sync (access_impl, + request->id, + app_id, + "", + title, + subtitle, + body, + g_variant_builder_end (&opt_builder), + &response, + &results, + NULL, + &error)) + { + g_warning ("AccessDialog call failed: %s", error->message); + g_clear_error (&error); + } + + allowed = response == 0; + } + else + { + allowed = permission != PERMISSION_NO; + if (permission == PERMISSION_UNSET) + set_permission (app_id, PERMISSION_YES); + } + + g_debug ("Setting autostart for %s to %s", app_id, + allowed && autostart_requested ? "enabled" : "disabled"); + + autostart_enabled = FALSE; + + commandline = xdp_app_info_rewrite_commandline (request->app_info, autostart_exec, + FALSE /* don't quote escape */); + if (commandline == NULL) + { + g_debug ("Autostart not supported for: %s", app_id); + } + else if (!xdp_dbus_impl_background_call_enable_autostart_sync (background_impl, + app_id, + allowed && autostart_requested, + (const char * const *)commandline, + autostart_flags, + &autostart_enabled, + NULL, + &error)) + { + g_warning ("EnableAutostart call failed: %s", error->message); + g_clear_error (&error); + } + + if (request->exported) + { + XdgDesktopPortalResponseEnum portal_response; + GVariantBuilder results; + + g_variant_builder_init (&results, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add (&results, "{sv}", "background", g_variant_new_boolean (allowed)); + g_variant_builder_add (&results, "{sv}", "autostart", g_variant_new_boolean (autostart_enabled)); + + if (allowed) + portal_response = XDG_DESKTOP_PORTAL_RESPONSE_SUCCESS; + else + portal_response = XDG_DESKTOP_PORTAL_RESPONSE_CANCELLED; + + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + portal_response, + g_variant_builder_end (&results)); + request_unexport (request); + } +} + +static gboolean +validate_reason (const char *key, + GVariant *value, + GVariant *options, + GError **error) +{ + const char *string = g_variant_get_string (value, NULL); + + if (g_utf8_strlen (string, -1) > 256) + { + g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Not accepting overly long reasons"); + return FALSE; + } + + return TRUE; +} + +static gboolean +validate_commandline (const char *key, + GVariant *value, + GVariant *options, + GError **error) +{ + gsize length; + const char **strv = g_variant_get_strv (value, &length); + + if (strv[0] == NULL) + { + g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Commandline can't be empty"); + return FALSE; + } + + if (g_utf8_strlen (strv[0], -1) > 256) + { + g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Not accepting overly long commandlines"); + return FALSE; + } + + if (length > 100) + { + g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Not accepting overly long commandlines"); + return FALSE; + } + + return TRUE; +} +static XdpOptionKey background_options[] = { + { "reason", G_VARIANT_TYPE_STRING, validate_reason }, + { "autostart", G_VARIANT_TYPE_BOOLEAN, NULL }, + { "commandline", G_VARIANT_TYPE_STRING_ARRAY, validate_commandline }, + { "dbus-activatable", G_VARIANT_TYPE_BOOLEAN, NULL }, +}; + +static gboolean +handle_request_background (XdpDbusBackground *object, + GDBusMethodInvocation *invocation, + const char *arg_window, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + g_autoptr(GError) error = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + g_autoptr(GTask) task = NULL; + GVariantBuilder opt_builder; + g_autoptr(GVariant) options = NULL; + + REQUEST_AUTOLOCK (request); + + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &opt_builder, + background_options, G_N_ELEMENTS (background_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + options = g_variant_ref_sink (g_variant_builder_end (&opt_builder)); + + g_object_set_data_full (G_OBJECT (request), "window", g_strdup (arg_window), g_free); + g_object_set_data_full (G_OBJECT (request), "options", g_variant_ref (options), (GDestroyNotify)g_variant_unref); + + impl_request = + xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (access_impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (access_impl)), + request->id, + NULL, &error); + if (!impl_request) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request (request, impl_request); + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + xdp_dbus_background_complete_request_background (object, invocation, request->id); + + task = g_task_new (object, NULL, NULL, NULL); + g_task_set_task_data (task, g_object_ref (request), g_object_unref); + g_task_run_in_thread (task, handle_request_background_in_thread_func); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +set_status_finished_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GDBusMethodInvocation *invocation; + g_autoptr(GError) error = NULL; + + g_assert (g_task_is_valid (result, source_object)); + + invocation = g_task_get_task_data (G_TASK (result)); + g_assert (invocation != NULL); + + if (g_task_propagate_boolean (G_TASK (result), &error)) + { + xdp_dbus_background_complete_set_status (XDP_DBUS_BACKGROUND (source_object), + invocation); + } + else + { + g_dbus_method_invocation_return_gerror (invocation, error); + } +} + +static void +handle_set_status_in_thread_func (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GDBusMethodInvocation *invocation = task_data; + g_autofree char *message = NULL; + InstanceData *data; + const char *id = NULL; + GVariant *options; + Call *call; + + call = call_from_invocation (invocation); + id = xdp_app_info_get_instance (call->app_info); + + options = g_object_get_data (G_OBJECT (invocation), "options"); + g_variant_lookup (options, "message", "s", &message); + + G_LOCK (applications); + data = g_hash_table_lookup (applications, id); + + if (!data) + { + g_autoptr(GHashTable) app_states = NULL; + g_autoptr(GPtrArray) instances = NULL; + FlatpakInstance *instance = NULL; + + instances = flatpak_instance_get_all (); + for (guint i = 0; i < instances->len; i++) + { + FlatpakInstance *aux = g_ptr_array_index (instances, i); + if (g_strcmp0 (id, flatpak_instance_get_id (aux)) == 0) + { + instance = aux; + break; + } + } + + if (!instance) + { + G_UNLOCK (applications); + g_task_return_new_error (task, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_FAILED, + "No sandboxed instance of the application found"); + return; + } + + app_states = get_app_states (); + if (app_states == NULL) + { + G_UNLOCK (applications); + g_task_return_new_error (task, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_FAILED, + "Could not fetch app state from backend"); + return; + } + + data = g_new0 (InstanceData, 1); + data->instance = g_object_ref (instance); + data->state = get_one_app_state (xdp_app_info_get_id (call->app_info), app_states); + g_hash_table_insert (applications, g_strdup (id), data); + } + + g_assert (data != NULL); + g_clear_pointer (&data->status_message, g_free); + data->status_message = g_steal_pointer (&message); + + G_UNLOCK (applications); + + update_background_monitor_properties (); + + g_task_return_boolean (task, TRUE); +} + +static gboolean +validate_message (const char *key, + GVariant *value, + GVariant *options, + GError **error) +{ + const char *string = g_variant_get_string (value, NULL); + + if (g_utf8_strlen (string, -1) > 96) + { + g_set_error (error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Status message is longer than 96 characters"); + return FALSE; + } + + if (strstr (string, "\n")) + { + g_set_error (error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Status message must not have newlines"); + return FALSE; + } + + return TRUE; +} + +static XdpOptionKey set_status_options[] = { + { "message", G_VARIANT_TYPE_STRING, validate_message }, +}; + +static gboolean +handle_set_status (XdpDbusBackground *object, + GDBusMethodInvocation *invocation, + GVariant *arg_options) +{ + g_autoptr(GVariant) options = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GTask) task = NULL; + GVariantBuilder opt_builder; + const char *id = NULL; + Call *call; + + call = call_from_invocation (invocation); + + g_debug ("Handling SetStatus call from %s", xdp_app_info_get_id (call->app_info)); + + if (xdp_app_info_is_host (call->app_info)) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Only sandboxed applications can set background status"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + id = xdp_app_info_get_instance (call->app_info); + if (!id) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_FAILED, + "No sandboxed instance of the application found"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &opt_builder, + set_status_options, + G_N_ELEMENTS (set_status_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + options = g_variant_ref_sink (g_variant_builder_end (&opt_builder)); + + g_object_set_data_full (G_OBJECT (invocation), + "options", + g_steal_pointer (&options), + (GDestroyNotify) g_variant_unref); + + task = g_task_new (object, NULL, set_status_finished_cb, NULL); + g_task_set_task_data (task, g_object_ref (invocation), g_object_unref); + g_task_run_in_thread (task, handle_set_status_in_thread_func); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +background_iface_init (XdpDbusBackgroundIface *iface) +{ + iface->handle_request_background = handle_request_background; + iface->handle_set_status = handle_set_status; +} + +static void +background_init (Background *background) +{ + xdp_dbus_background_set_version (XDP_DBUS_BACKGROUND (background), 2); +} + +static void +background_class_init (BackgroundClass *klass) +{ +} + +GDBusInterfaceSkeleton * +background_create (GDBusConnection *connection, + const char *dbus_name_access, + const char *dbus_name_background) +{ + g_autofree char *instance_path = NULL; + g_autoptr(GFile) instance_dir = NULL; + g_autoptr(GError) error = NULL; + + access_impl = xdp_dbus_impl_access_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_NONE, + dbus_name_access, + DESKTOP_PORTAL_OBJECT_PATH, + NULL, + &error); + if (access_impl == NULL) + { + g_warning ("Failed to create access proxy: %s", error->message); + return NULL; + } + + g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (access_impl), G_MAXINT); + + background_impl = xdp_dbus_impl_background_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_NONE, + dbus_name_background, + DESKTOP_PORTAL_OBJECT_PATH, + NULL, + &error); + if (background_impl == NULL) + { + g_warning ("Failed to create background proxy: %s", error->message); + return NULL; + } + + g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (background_impl), G_MAXINT); + background = g_object_new (background_get_type (), NULL); + background->monitor = background_monitor_new (NULL, &error); + if (background->monitor == NULL) + { + g_warning ("Failed to create background monitor: %s", error->message); + return NULL; + } + + start_background_monitor (); + + g_signal_connect (background_impl, "running-applications-changed", + G_CALLBACK (running_apps_changed), NULL); + + /* FIXME: it would be better if libflatpak had a monitor api for this */ + instance_path = g_build_filename (g_get_user_runtime_dir (), ".flatpak", NULL); + instance_dir = g_file_new_for_path (instance_path); + instance_monitor = g_file_monitor_directory (instance_dir, G_FILE_MONITOR_NONE, NULL, &error); + if (!instance_monitor) + g_warning ("Failed to create a monitor for %s: %s", instance_path, error->message); + else + g_signal_connect (instance_monitor, "changed", G_CALLBACK (instances_changed), NULL); + + return G_DBUS_INTERFACE_SKELETON (background); +} diff --git a/src/background.h b/src/background.h new file mode 100644 index 0000000..f66b208 --- /dev/null +++ b/src/background.h @@ -0,0 +1,28 @@ +/* + * Copyright © 2019 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + + +#pragma once + +#include + +GDBusInterfaceSkeleton * background_create (GDBusConnection *connection, + const char *dbus_name_access, + const char *dbus_name_background); diff --git a/src/call.c b/src/call.c new file mode 100644 index 0000000..781b993 --- /dev/null +++ b/src/call.c @@ -0,0 +1,47 @@ +/* + * Copyright © 2018 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + */ + +#include "call.h" + +static void +call_free (Call *call) +{ + xdp_app_info_unref (call->app_info); + g_free (call->sender); + g_free (call); +} + +void +call_init_invocation (GDBusMethodInvocation *invocation, + XdpAppInfo *app_info) +{ + Call *call; + + call = g_new0 (Call, 1); + call->app_info = xdp_app_info_ref (app_info); + call->sender = g_strdup (g_dbus_method_invocation_get_sender (invocation)); + + g_object_set_data_full (G_OBJECT (invocation), "call", + call, (GDestroyNotify) call_free); +} + +Call * +call_from_invocation (GDBusMethodInvocation *invocation) +{ + return g_object_get_data (G_OBJECT (invocation), "call"); +} diff --git a/src/call.h b/src/call.h new file mode 100644 index 0000000..1ff8b31 --- /dev/null +++ b/src/call.h @@ -0,0 +1,33 @@ +/* + * Copyright © 2018 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + */ + +#pragma once + +#include +#include "xdp-utils.h" + +typedef struct _Call +{ + XdpAppInfo *app_info; + char *sender; +} Call; + +void call_init_invocation (GDBusMethodInvocation *invocation, + XdpAppInfo *app_info); + +Call *call_from_invocation (GDBusMethodInvocation *invocation); diff --git a/src/camera.c b/src/camera.c new file mode 100644 index 0000000..8ae36d2 --- /dev/null +++ b/src/camera.c @@ -0,0 +1,455 @@ +/* + * Copyright © 2018-2019 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + */ + +#include "config.h" + +#include +#include +#include +#include + +#include "device.h" +#include "request.h" +#include "permissions.h" +#include "pipewire.h" +#include "xdp-dbus.h" +#include "xdp-impl-dbus.h" +#include "xdp-utils.h" + +static XdpDbusImplLockdown *lockdown; + +typedef struct _Camera Camera; +typedef struct _CameraClass CameraClass; + +struct _Camera +{ + XdpDbusCameraSkeleton parent_instance; + + PipeWireRemote *pipewire_remote; + GSource *pipewire_source; + GFileMonitor *pipewire_socket_monitor; + int64_t connect_timestamps[10]; + int connect_timestamps_i; + GHashTable *cameras; +}; + +struct _CameraClass +{ + XdpDbusCameraSkeletonClass parent_class; +}; + +static Camera *camera; + +GType camera_get_type (void); +static void camera_iface_init (XdpDbusCameraIface *iface); + +G_DEFINE_TYPE_WITH_CODE (Camera, camera, XDP_DBUS_TYPE_CAMERA_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_CAMERA, + camera_iface_init)) + +static gboolean +create_pipewire_remote (Camera *camera, + GError **error); + +static void +handle_access_camera_in_thread_func (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + Request *request = (Request *)task_data; + const char *app_id; + gboolean allowed; + + app_id = (const char *)g_object_get_data (G_OBJECT (request), "app-id"); + + allowed = device_query_permission_sync (app_id, "camera", request); + + REQUEST_AUTOLOCK (request); + + if (request->exported) + { + GVariantBuilder results; + guint32 response; + + g_variant_builder_init (&results, G_VARIANT_TYPE_VARDICT); + + response = allowed ? XDG_DESKTOP_PORTAL_RESPONSE_SUCCESS + : XDG_DESKTOP_PORTAL_RESPONSE_CANCELLED; + g_debug ("Camera: sending response %d", response); + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + response, + g_variant_builder_end (&results)); + request_unexport (request); + } +} + +static gboolean +handle_access_camera (XdpDbusCamera *object, + GDBusMethodInvocation *invocation, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + const char *app_id; + g_autoptr(GTask) task = NULL; + + if (xdp_dbus_impl_lockdown_get_disable_camera (lockdown)) + { + g_debug ("Camera access disabled"); + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Camera access disabled"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + REQUEST_AUTOLOCK (request); + + app_id = xdp_app_info_get_id (request->app_info); + + + g_object_set_data_full (G_OBJECT (request), "app-id", g_strdup (app_id), g_free); + + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + xdp_dbus_camera_complete_access_camera (object, invocation, request->id); + + task = g_task_new (object, NULL, NULL, NULL); + g_task_set_task_data (task, g_object_ref (request), g_object_unref); + g_task_run_in_thread (task, handle_access_camera_in_thread_func); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static PipeWireRemote * +open_pipewire_camera_remote (const char *app_id, + GError **error) +{ + PipeWireRemote *remote; + struct pw_permission permission_items[3]; + struct pw_properties *pipewire_properties; + + pipewire_properties = + pw_properties_new ("pipewire.access.portal.app_id", app_id, + "pipewire.access.portal.media_roles", "Camera", + NULL); + remote = pipewire_remote_new_sync (pipewire_properties, + NULL, NULL, NULL, NULL, + error); + if (!remote) + return NULL; + + /* + * Hide all existing and future nodes by default. PipeWire will use the + * permission store to set up permissions. + */ + permission_items[0] = PW_PERMISSION_INIT (PW_ID_CORE, PW_PERM_RWX); + permission_items[1] = PW_PERMISSION_INIT (remote->node_factory_id, PW_PERM_R); + permission_items[2] = PW_PERMISSION_INIT (PW_ID_ANY, 0); + + pw_client_update_permissions (pw_core_get_client(remote->core), + G_N_ELEMENTS (permission_items), + permission_items); + + pipewire_remote_roundtrip (remote); + + return remote; +} + +static gboolean +handle_open_pipewire_remote (XdpDbusCamera *object, + GDBusMethodInvocation *invocation, + GUnixFDList *in_fd_list, + GVariant *arg_options) +{ + g_autoptr(XdpAppInfo) app_info = NULL; + const char *app_id; + Permission permission; + g_autoptr(GUnixFDList) out_fd_list = NULL; + int fd; + int fd_id; + g_autoptr(GError) error = NULL; + PipeWireRemote *remote; + + if (xdp_dbus_impl_lockdown_get_disable_camera (lockdown)) + { + g_debug ("Camera access disabled"); + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Camera access disabled"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + app_info = xdp_invocation_lookup_app_info_sync (invocation, NULL, &error); + app_id = xdp_app_info_get_id (app_info); + permission = device_get_permission_sync (app_id, "camera"); + if (permission != PERMISSION_YES) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Permission denied"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + remote = open_pipewire_camera_remote (app_id, &error); + if (!remote) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_FAILED, + "Failed to open PipeWire remote: %s", + error->message); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + out_fd_list = g_unix_fd_list_new (); + fd = pw_core_steal_fd (remote->core); + fd_id = g_unix_fd_list_append (out_fd_list, fd, &error); + close (fd); + pipewire_remote_destroy (remote); + + if (fd_id == -1) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_FAILED, + "Failed to append fd: %s", + error->message); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + xdp_dbus_camera_complete_open_pipewire_remote (object, invocation, + out_fd_list, + g_variant_new_handle (fd_id)); + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +camera_iface_init (XdpDbusCameraIface *iface) +{ + iface->handle_access_camera = handle_access_camera; + iface->handle_open_pipewire_remote = handle_open_pipewire_remote; +} + +static void +global_added_cb (PipeWireRemote *remote, + uint32_t id, + const char *type, + const struct spa_dict *props, + gpointer user_data) +{ + Camera *camera = user_data; + const struct spa_dict_item *media_class; + const struct spa_dict_item *media_role; + + if (strcmp(type, PW_TYPE_INTERFACE_Node) != 0) + return; + + if (!props) + return; + + media_class = spa_dict_lookup_item (props, PW_KEY_MEDIA_CLASS); + if (!media_class) + return; + + if (g_strcmp0 (media_class->value, "Video/Source") != 0) + return; + + media_role = spa_dict_lookup_item (props, PW_KEY_MEDIA_ROLE); + if (!media_role) + return; + + if (g_strcmp0 (media_role->value, "Camera") != 0) + return; + + g_hash_table_add (camera->cameras, GINT_TO_POINTER (id)); + + xdp_dbus_camera_set_is_camera_present (XDP_DBUS_CAMERA (camera), + g_hash_table_size (camera->cameras) > 0); +} + +static void global_removed_cb (PipeWireRemote *remote, + uint32_t id, + gpointer user_data) +{ + Camera *camera = user_data; + + g_hash_table_remove (camera->cameras, GINT_TO_POINTER (id)); + + xdp_dbus_camera_set_is_camera_present (XDP_DBUS_CAMERA (camera), + g_hash_table_size (camera->cameras) > 0); +} + +static void +pipewire_remote_error_cb (gpointer data, + gpointer user_data) +{ + Camera *camera = user_data; + g_autoptr(GError) error = NULL; + + g_hash_table_remove_all (camera->cameras); + xdp_dbus_camera_set_is_camera_present (XDP_DBUS_CAMERA (camera), FALSE); + + g_clear_pointer (&camera->pipewire_source, g_source_destroy); + g_clear_pointer (&camera->pipewire_remote, pipewire_remote_destroy); + + if (!create_pipewire_remote (camera, &error)) + g_warning ("Failed connect to PipeWire: %s", error->message); +} + +static gboolean +create_pipewire_remote (Camera *camera, + GError **error) +{ + struct pw_properties *pipewire_properties; + const int n_connect_retries = G_N_ELEMENTS (camera->connect_timestamps); + int64_t now; + int max_retries_ago_i; + int64_t max_retries_ago; + + now = g_get_monotonic_time (); + camera->connect_timestamps[camera->connect_timestamps_i] = now; + + max_retries_ago_i = (camera->connect_timestamps_i + 1) % n_connect_retries; + max_retries_ago = camera->connect_timestamps[max_retries_ago_i]; + + camera->connect_timestamps_i = + (camera->connect_timestamps_i + 1) % n_connect_retries; + + if (max_retries_ago && + now - max_retries_ago < G_USEC_PER_SEC * 10) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Tried to reconnect to PipeWire too often, giving up"); + return FALSE; + } + + pipewire_properties = pw_properties_new ("pipewire.access.portal.is_portal", "true", + "portal.monitor", "Camera", + NULL); + camera->pipewire_remote = pipewire_remote_new_sync (pipewire_properties, + global_added_cb, + global_removed_cb, + pipewire_remote_error_cb, + camera, + error); + if (!camera->pipewire_remote) + return FALSE; + + camera->pipewire_source = + pipewire_remote_create_source (camera->pipewire_remote); + + return TRUE; +} + +static void +on_pipewire_socket_changed (GFileMonitor *monitor, + GFile *file, + GFile *other_file, + GFileMonitorEvent event_type, + Camera *camera) +{ + g_autoptr(GError) error = NULL; + + if (event_type != G_FILE_MONITOR_EVENT_CREATED) + return; + + if (camera->pipewire_remote) + { + g_debug ("PipeWire socket created after remote was created"); + return; + } + + g_debug ("PipeWireSocket created, tracking cameras"); + + if (!create_pipewire_remote (camera, &error)) + g_warning ("Failed connect to PipeWire: %s", error->message); +} + +static gboolean +init_camera_tracker (Camera *camera, + GError **error) +{ + g_autofree char *pipewire_socket_path = NULL; + GFile *pipewire_socket; + g_autoptr(GError) local_error = NULL; + + pipewire_socket_path = g_strdup_printf ("%s/pipewire-0", + g_get_user_runtime_dir ()); + pipewire_socket = g_file_new_for_path (pipewire_socket_path); + camera->pipewire_socket_monitor = + g_file_monitor_file (pipewire_socket, G_FILE_MONITOR_NONE, NULL, error); + if (!camera->pipewire_socket_monitor) + return FALSE; + + g_signal_connect (camera->pipewire_socket_monitor, + "changed", + G_CALLBACK (on_pipewire_socket_changed), + camera); + + camera->cameras = g_hash_table_new (NULL, NULL); + + if (!create_pipewire_remote (camera, &local_error)) + g_warning ("Failed connect to PipeWire: %s", local_error->message); + + return TRUE; +} + +static void +camera_finalize (GObject *object) +{ + Camera *camera = (Camera *)object; + + g_clear_pointer (&camera->pipewire_source, g_source_destroy); + g_clear_pointer (&camera->pipewire_remote, pipewire_remote_destroy); + g_clear_pointer (&camera->cameras, g_hash_table_unref); + + G_OBJECT_CLASS (camera_parent_class)->finalize (object); +} + +static void +camera_init (Camera *camera) +{ + g_autoptr(GError) error = NULL; + + xdp_dbus_camera_set_version (XDP_DBUS_CAMERA (camera), 1); + + if (!init_camera_tracker (camera, &error)) + g_warning ("Failed to track cameras: %s", error->message); +} + +static void +camera_class_init (CameraClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = camera_finalize; +} + +GDBusInterfaceSkeleton * +camera_create (GDBusConnection *connection, + gpointer lockdown_proxy) +{ + lockdown = lockdown_proxy; + + camera = g_object_new (camera_get_type (), NULL); + + return G_DBUS_INTERFACE_SKELETON (camera); +} diff --git a/src/camera.h b/src/camera.h new file mode 100644 index 0000000..8163a99 --- /dev/null +++ b/src/camera.h @@ -0,0 +1,24 @@ +/* + * Copyright © 2018 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + */ + +#pragma once + +#include + +GDBusInterfaceSkeleton * camera_create (GDBusConnection *connection, + gpointer lockdown_proxy); diff --git a/src/clipboard.c b/src/clipboard.c new file mode 100644 index 0000000..edf1358 --- /dev/null +++ b/src/clipboard.c @@ -0,0 +1,539 @@ +/* + * Copyright 2022 Google LLC + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + */ +#include "config.h" + +#include +#include + +#include "clipboard.h" +#include "remote-desktop.h" +#include "session.h" +#include "xdp-dbus.h" +#include "xdp-impl-dbus.h" +#include "xdp-utils.h" + +typedef struct _Clipboard Clipboard; +typedef struct _ClipboardClass ClipboardClass; + +struct _Clipboard +{ + XdpDbusClipboardSkeleton parent_instance; +}; + +struct _ClipboardClass +{ + XdpDbusClipboardSkeletonClass parent_class; +}; + +static XdpDbusImplClipboard *impl; +static Clipboard *clipboard; + +GType clipboard_get_type (void) G_GNUC_CONST; +static void clipboard_iface_init (XdpDbusClipboardIface *iface); + +G_DEFINE_TYPE_WITH_CODE (Clipboard, + clipboard, + XDP_DBUS_TYPE_CLIPBOARD_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_CLIPBOARD, + clipboard_iface_init)) + +static XdpOptionKey clipboard_set_selection_options[] = { + { "mime_types", G_VARIANT_TYPE_STRING_ARRAY, NULL }, +}; + +static gboolean +handle_request_clipboard (XdpDbusClipboard *object, + GDBusMethodInvocation *invocation, + const char *arg_session_handle, + GVariant *arg_options) +{ + Call *call = call_from_invocation (invocation); + Session *session; + RemoteDesktopSession *remote_desktop_session; + + session = acquire_session_from_call (arg_session_handle, call); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + if (!is_remote_desktop_session (session)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session type"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + remote_desktop_session = (RemoteDesktopSession *)session; + + if (!remote_desktop_session_can_request_clipboard (remote_desktop_session)) + { + g_dbus_method_invocation_return_error ( + invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "Invalid state"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + xdp_dbus_impl_clipboard_call_request_clipboard ( + impl, session->id, arg_options, NULL, NULL, NULL); + + xdp_dbus_clipboard_complete_request_clipboard (object, invocation); + remote_desktop_session_clipboard_requested (remote_desktop_session); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_set_selection (XdpDbusClipboard *object, + GDBusMethodInvocation *invocation, + const char *arg_session_handle, + GVariant *arg_options) +{ + Call *call = call_from_invocation (invocation); + Session *session; + GVariantBuilder options_builder; + GVariant *options; + g_autoptr(GError) error = NULL; + + session = acquire_session_from_call (arg_session_handle, call); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + if (!is_remote_desktop_session (session)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session type"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + else if (!remote_desktop_session_is_clipboard_enabled ( + (RemoteDesktopSession *)session)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Clipboard not enabled"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, + &options_builder, + clipboard_set_selection_options, + G_N_ELEMENTS (clipboard_set_selection_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + options = g_variant_builder_end (&options_builder); + + xdp_dbus_impl_clipboard_call_set_selection ( + impl, arg_session_handle, options, NULL, NULL, NULL); + + xdp_dbus_clipboard_complete_set_selection (object, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +selection_write_done (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GDBusMethodInvocation) invocation = g_steal_pointer (&user_data); + g_autoptr(GUnixFDList) out_fd_list = NULL; + g_autoptr(GUnixFDList) fd_list = NULL; + g_autoptr(GVariant) fd_handle = NULL; + g_autoptr(GError) error = NULL; + int fd; + int fd_id; + int out_fd_id = -1; + + if (!xdp_dbus_impl_clipboard_call_selection_write_finish ( + impl, &fd_handle, &fd_list, res, &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("A backend call failed: %s", error->message); + } + + out_fd_list = g_unix_fd_list_new (); + + if (fd_handle) + { + fd_id = g_variant_get_handle (fd_handle); + fd = g_unix_fd_list_get (fd_list, fd_id, &error); + + out_fd_id = g_unix_fd_list_append (out_fd_list, fd, &error); + + close (fd); + + if (out_fd_id == -1) + { + g_dbus_method_invocation_return_error ( + invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_FAILED, + "Failed to append fd: %s", + error->message); + } + } + + xdp_dbus_clipboard_complete_selection_write ( + NULL, + invocation, + out_fd_list, + g_variant_new_handle (out_fd_id)); +} + +static gboolean +handle_selection_write (XdpDbusClipboard *object, + GDBusMethodInvocation *invocation, + GUnixFDList *in_fd_list, + const char *arg_session_handle, + guint arg_serial) +{ + Call *call = call_from_invocation (invocation); + Session *session; + + session = acquire_session_from_call (arg_session_handle, call); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + if (!is_remote_desktop_session (session)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session type"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + else if (!remote_desktop_session_is_clipboard_enabled ( + (RemoteDesktopSession *)session)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Clipboard not enabled"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + xdp_dbus_impl_clipboard_call_selection_write (impl, + arg_session_handle, + arg_serial, + NULL, + NULL, + selection_write_done, + g_object_ref (invocation)); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_selection_write_done (XdpDbusClipboard *object, + GDBusMethodInvocation *invocation, + const char *arg_session_handle, + guint arg_serial, + gboolean arg_success) +{ + Call *call = call_from_invocation (invocation); + Session *session; + + session = acquire_session_from_call (arg_session_handle, call); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + if (!is_remote_desktop_session (session)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session type"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + else if (!remote_desktop_session_is_clipboard_enabled ( + (RemoteDesktopSession *)session)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Clipboard not enabled"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + xdp_dbus_impl_clipboard_call_selection_write_done ( + impl, arg_session_handle, arg_serial, arg_success, NULL, NULL, NULL); + + xdp_dbus_clipboard_complete_selection_write_done (object, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +selection_read_done (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GDBusMethodInvocation) invocation = g_steal_pointer (&user_data); + g_autoptr(GUnixFDList) out_fd_list = NULL; + g_autoptr(GUnixFDList) fd_list = NULL; + g_autoptr(GVariant) fd_handle = NULL; + g_autoptr(GError) error = NULL; + + int fd; + int fd_id; + int out_fd_id; + + if (!xdp_dbus_impl_clipboard_call_selection_read_finish ( + impl, &fd_handle, &fd_list, res, &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("A backend call failed: %s", error->message); + } + + fd_id = g_variant_get_handle (fd_handle); + fd = g_unix_fd_list_get (fd_list, fd_id, &error); + + out_fd_list = g_unix_fd_list_new (); + out_fd_id = g_unix_fd_list_append (out_fd_list, fd, &error); + close (fd); + + if (out_fd_id == -1) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_FAILED, + "Failed to append fd: %s", + error->message); + } + + xdp_dbus_clipboard_complete_selection_read ( + NULL, invocation, out_fd_list, g_variant_new_handle (out_fd_id)); +} + +static gboolean +handle_selection_read (XdpDbusClipboard *object, + GDBusMethodInvocation *invocation, + GUnixFDList *in_fd_list, + const char *arg_session_handle, + const char *arg_mime_type) +{ + Call *call = call_from_invocation (invocation); + Session *session; + + session = acquire_session_from_call (arg_session_handle, call); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + if (!is_remote_desktop_session (session)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session type"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + else if (!remote_desktop_session_is_clipboard_enabled ( + (RemoteDesktopSession *)session)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Clipboard not enabled"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + xdp_dbus_impl_clipboard_call_selection_read (impl, + arg_session_handle, + arg_mime_type, + NULL, + NULL, + selection_read_done, + g_object_ref (invocation)); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +clipboard_iface_init (XdpDbusClipboardIface *iface) +{ + iface->handle_request_clipboard = handle_request_clipboard; + + iface->handle_selection_read = handle_selection_read; + iface->handle_selection_write = handle_selection_write; + iface->handle_set_selection = handle_set_selection; + iface->handle_selection_write_done = handle_selection_write_done; +} + +static void +clipboard_init (Clipboard *clipboard) +{ + xdp_dbus_clipboard_set_version (XDP_DBUS_CLIPBOARD (clipboard), 1); +} + +static void +clipboard_class_init (ClipboardClass *klass) +{ +} + +static void +selection_transfer_cb (XdpDbusImplClipboard *impl, + const char *arg_session_handle, + const char *arg_mime_type, + guint arg_serial, + gpointer data) +{ + GDBusConnection *connection = + g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)); + Session *session; + + session = lookup_session (arg_session_handle); + if (!session) + { + g_warning ("Cannot find session"); + return; + } + + SESSION_AUTOLOCK_UNREF (session); + + RemoteDesktopSession *remote_desktop_session = + (RemoteDesktopSession *)session; + + if (remote_desktop_session && + remote_desktop_session_is_clipboard_enabled (remote_desktop_session) && + !session->closed) + { + g_dbus_connection_emit_signal ( + connection, + session->sender, + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.Clipboard", + "SelectionTransfer", + g_variant_new ("(osu)", arg_session_handle, arg_mime_type, arg_serial), + NULL); + } +} + +static void +selection_owner_changed_cb (XdpDbusImplClipboard *impl, + const char *arg_session_handle, + GVariant *arg_options, + gpointer data) +{ + GDBusConnection *connection = + g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)); + Session *session; + + session = lookup_session (arg_session_handle); + if (!session) + { + g_warning ("Cannot find session"); + return; + } + + SESSION_AUTOLOCK_UNREF (session); + + RemoteDesktopSession *remote_desktop_session = + (RemoteDesktopSession *)session; + + if (remote_desktop_session && + remote_desktop_session_is_clipboard_enabled (remote_desktop_session) && + !session->closed) + { + g_dbus_connection_emit_signal ( + connection, + session->sender, + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.Clipboard", + "SelectionOwnerChanged", + g_variant_new ("(o@a{sv})", arg_session_handle, arg_options), + NULL); + } +} + +GDBusInterfaceSkeleton * +clipboard_create (GDBusConnection *connection, + const char *dbus_name) +{ + g_autoptr(GError) error = NULL; + + impl = xdp_dbus_impl_clipboard_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_NONE, + dbus_name, + DESKTOP_PORTAL_OBJECT_PATH, + NULL, + &error); + if (impl == NULL) + { + g_warning ("Failed to create clipboard: %s", error->message); + return NULL; + } + + g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (impl), G_MAXINT); + + clipboard = g_object_new (clipboard_get_type (), NULL); + + g_signal_connect ( + impl, "selection-transfer", G_CALLBACK (selection_transfer_cb), clipboard); + + g_signal_connect (impl, + "selection-owner-changed", + G_CALLBACK (selection_owner_changed_cb), + clipboard); + + return G_DBUS_INTERFACE_SKELETON (clipboard); +} diff --git a/src/clipboard.h b/src/clipboard.h new file mode 100644 index 0000000..a56ed3f --- /dev/null +++ b/src/clipboard.h @@ -0,0 +1,26 @@ +/* + * Copyright 2022 Google LLC + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + */ + +#pragma once + +#include + +#include "session.h" + +GDBusInterfaceSkeleton *clipboard_create (GDBusConnection *connection, + const char *dbus_name); diff --git a/src/device.c b/src/device.c new file mode 100644 index 0000000..81cbf63 --- /dev/null +++ b/src/device.c @@ -0,0 +1,365 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +#include "config.h" + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include "device.h" +#include "request.h" +#include "permissions.h" +#include "xdp-dbus.h" +#include "xdp-impl-dbus.h" +#include "xdp-utils.h" + +#define PERMISSION_TABLE "devices" + +typedef struct _Device Device; +typedef struct _DeviceClass DeviceClass; + +struct _Device +{ + XdpDbusDeviceSkeleton parent_instance; +}; + +struct _DeviceClass +{ + XdpDbusDeviceSkeletonClass parent_class; +}; + +static XdpDbusImplAccess *impl; +static Device *device; +static XdpDbusImplLockdown *lockdown; + +GType device_get_type (void) G_GNUC_CONST; +static void device_iface_init (XdpDbusDeviceIface *iface); + +G_DEFINE_TYPE_WITH_CODE (Device, device, XDP_DBUS_TYPE_DEVICE_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_DEVICE, + device_iface_init)); + +static const char *known_devices[] = { + "microphone", + "speakers", + "camera", + NULL +}; + +Permission +device_get_permission_sync (const char *app_id, + const char *device) +{ + return get_permission_sync (app_id, PERMISSION_TABLE, device); +} + +gboolean +device_query_permission_sync (const char *app_id, + const char *device, + Request *request) +{ + Permission permission; + gboolean allowed; + + permission = device_get_permission_sync (app_id, device); + if (permission == PERMISSION_ASK || permission == PERMISSION_UNSET) + { + GVariantBuilder opt_builder; + g_autofree char *title = NULL; + g_autofree char *subtitle = NULL; + g_autofree char *body = NULL; + guint32 response = 2; + g_autoptr(GVariant) results = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GAppInfo) info = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + + if (app_id[0] != 0) + { + g_autofree char *desktop_id; + desktop_id = g_strconcat (app_id, ".desktop", NULL); + info = (GAppInfo*)g_desktop_app_info_new (desktop_id); + } + + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + + if (strcmp (device, "microphone") == 0) + { + g_variant_builder_add (&opt_builder, "{sv}", "icon", g_variant_new_string ("audio-input-microphone-symbolic")); + + title = g_strdup (_("Turn On Microphone?")); + body = g_strdup (_("Access to your microphone can be changed " + "at any time from the privacy settings.")); + + if (info == NULL) + subtitle = g_strdup (_("An application wants to use your microphone.")); + else + subtitle = g_strdup_printf (_("%s wants to use your microphone."), g_app_info_get_display_name (info)); + } + else if (strcmp (device, "speakers") == 0) + { + g_variant_builder_add (&opt_builder, "{sv}", "icon", g_variant_new_string ("audio-speakers-symbolic")); + + title = g_strdup (_("Turn On Speakers?")); + body = g_strdup (_("Access to your speakers can be changed " + "at any time from the privacy settings.")); + + if (info == NULL) + subtitle = g_strdup (_("An application wants to play sound.")); + else + subtitle = g_strdup_printf (_("%s wants to play sound."), g_app_info_get_display_name (info)); + } + else if (strcmp (device, "camera") == 0) + { + g_variant_builder_add (&opt_builder, "{sv}", "icon", g_variant_new_string ("camera-web-symbolic")); + + title = g_strdup (_("Turn On Camera?")); + body = g_strdup (_("Access to your camera can be changed " + "at any time from the privacy settings.")); + + if (info == NULL) + subtitle = g_strdup (_("An application wants to use your camera.")); + else + subtitle = g_strdup_printf (_("%s wants to use your camera."), g_app_info_get_display_name (info)); + } + + impl_request = xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, &error); + if (!impl_request) + return FALSE; + + request_set_impl_request (request, impl_request); + + g_debug ("Calling backend for device access to: %s", device); + + if (!xdp_dbus_impl_access_call_access_dialog_sync (impl, + request->id, + app_id, + "", + title, + subtitle, + body, + g_variant_builder_end (&opt_builder), + &response, + &results, + NULL, + &error)) + { + g_warning ("A backend call failed: %s", error->message); + } + + allowed = response == 0; + + if (permission == PERMISSION_UNSET) + set_permission_sync (app_id, PERMISSION_TABLE, device, allowed ? PERMISSION_YES : PERMISSION_NO); + } + else + allowed = permission == PERMISSION_YES ? TRUE : FALSE; + + return allowed; +} + +static void +handle_access_device_in_thread (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + Request *request = (Request *)task_data; + const char *app_id; + const char *device; + gboolean allowed; + + REQUEST_AUTOLOCK (request); + + app_id = (const char *)g_object_get_data (G_OBJECT (request), "app-id"); + device = (const char *)g_object_get_data (G_OBJECT (request), "device"); + + allowed = device_query_permission_sync (app_id, device, request); + + if (request->exported) + { + GVariantBuilder results; + + g_variant_builder_init (&results, G_VARIANT_TYPE_VARDICT); + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + allowed ? XDG_DESKTOP_PORTAL_RESPONSE_SUCCESS + : XDG_DESKTOP_PORTAL_RESPONSE_CANCELLED, + g_variant_builder_end (&results)); + request_unexport (request); + } +} + +static gboolean +handle_access_device (XdpDbusDevice *object, + GDBusMethodInvocation *invocation, + guint32 pid, + const char * const *devices, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + g_autoptr(XdpAppInfo) app_info = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + g_autoptr(GTask) task = NULL; + + if (g_strv_length ((char **)devices) != 1 || !g_strv_contains (known_devices, devices[0])) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Invalid devices requested"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + if (g_str_equal (devices[0], "microphone") && + xdp_dbus_impl_lockdown_get_disable_microphone (lockdown)) + { + g_debug ("Microphone access disabled"); + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Microphone access disabled"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + if (g_str_equal (devices[0], "camera") && + xdp_dbus_impl_lockdown_get_disable_camera (lockdown)) + { + g_debug ("Camera access disabled"); + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Camera access disabled"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + if (g_str_equal (devices[0], "speakers") && + xdp_dbus_impl_lockdown_get_disable_sound_output (lockdown)) + { + g_debug ("Speaker access disabled"); + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Speaker access disabled"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + REQUEST_AUTOLOCK (request); + + if (!xdp_app_info_is_host (request->app_info)) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "This call is not available inside the sandbox"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + app_info = xdp_get_app_info_from_pid (pid, &error); + if (app_info == NULL) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Invalid pid requested"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_object_set_data_full (G_OBJECT (request), "app-id", g_strdup (xdp_app_info_get_id (app_info)), g_free); + g_object_set_data_full (G_OBJECT (request), "device", g_strdup (devices[0]), g_free); + + impl_request = xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, &error); + if (!impl_request) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request (request, impl_request); + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + xdp_dbus_device_complete_access_device (object, invocation, request->id); + + task = g_task_new (object, NULL, NULL, NULL); + g_task_set_task_data (task, g_object_ref (request), g_object_unref); + g_task_run_in_thread (task, handle_access_device_in_thread); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +device_iface_init (XdpDbusDeviceIface *iface) +{ + iface->handle_access_device = handle_access_device; +} + +static void +device_init (Device *device) +{ + xdp_dbus_device_set_version (XDP_DBUS_DEVICE (device), 1); +} + +static void +device_class_init (DeviceClass *klass) +{ +} + +GDBusInterfaceSkeleton * +device_create (GDBusConnection *connection, + const char *dbus_name, + gpointer lockdown_proxy) +{ + g_autoptr(GError) error = NULL; + + lockdown = lockdown_proxy; + + impl = xdp_dbus_impl_access_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_NONE, + dbus_name, + DESKTOP_PORTAL_OBJECT_PATH, + NULL, + &error); + if (impl == NULL) + { + g_warning ("Failed to create access proxy: %s", error->message); + return NULL; + } + + g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (impl), G_MAXINT); + + device = g_object_new (device_get_type (), NULL); + + return G_DBUS_INTERFACE_SKELETON (device); +} diff --git a/src/device.h b/src/device.h new file mode 100644 index 0000000..74d1efb --- /dev/null +++ b/src/device.h @@ -0,0 +1,37 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +#pragma once + +#include + +#include "request.h" +#include "permissions.h" + +Permission device_get_permission_sync (const char *app_id, + const char *device); + +gboolean device_query_permission_sync (const char *app_id, + const char *device, + Request *request); + +GDBusInterfaceSkeleton * device_create (GDBusConnection *connection, + const char *dbus_name, + gpointer lockdown); diff --git a/src/documents.c b/src/documents.c new file mode 100644 index 0000000..711f96c --- /dev/null +++ b/src/documents.c @@ -0,0 +1,246 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Alexander Larsson + * Matthias Clasen + */ + +#include "config.h" + +#include + +#include +#include +#include + +#include +#include + +#include "xdp-dbus.h" +#include "xdp-utils.h" +#include "documents.h" +#include "document-enums.h" + +static XdpDbusDocuments *documents = NULL; +static char *documents_mountpoint = NULL; + +void +init_document_proxy (GDBusConnection *connection) +{ + documents = xdp_dbus_documents_proxy_new_sync (connection, 0, + "org.freedesktop.portal.Documents", + "/org/freedesktop/portal/documents", + NULL, NULL); + xdp_dbus_documents_call_get_mount_point_sync (documents, + &documents_mountpoint, + NULL, NULL); + xdp_set_documents_mountpoint (documents_mountpoint); +} + +char * +register_document (const char *uri, + const char *app_id, + DocumentFlags flags, + GError **error) +{ + g_autofree char *doc_id = NULL; + g_auto(GStrv) doc_ids = NULL; + g_autofree char *path = NULL; + g_autofree char *basename = NULL; + g_autofree char *dirname = NULL; + GUnixFDList *fd_list = NULL; + int fd, fd_in; + g_autoptr(GFile) file = NULL; + gboolean ret = FALSE; + const char *permissions[5]; + g_autofree char *doc_path = NULL; + int i; + int version; + gboolean handled_permissions = FALSE; + DocumentAddFullFlags full_flags; + + g_return_val_if_fail (app_id != NULL && *app_id != '\0', NULL); + + file = g_file_new_for_uri (uri); + path = g_file_get_path (file); + basename = g_path_get_basename (path); + dirname = g_path_get_dirname (path); + + if (flags & DOCUMENT_FLAG_FOR_SAVE) + fd = open (dirname, O_PATH | O_CLOEXEC); + else + fd = open (path, O_PATH | O_CLOEXEC); + + if (fd == -1) + { + g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errno), + "Failed to open %s", uri); + return NULL; + } + fd_list = g_unix_fd_list_new (); + fd_in = g_unix_fd_list_append (fd_list, fd, error); + close (fd); + + if (fd_in == -1) + return NULL; + + i = 0; + permissions[i++] = "read"; + if ((flags & DOCUMENT_FLAG_WRITABLE) || (flags & DOCUMENT_FLAG_FOR_SAVE)) + permissions[i++] = "write"; + permissions[i++] = "grant-permissions"; + if (flags & DOCUMENT_FLAG_DELETABLE) + permissions[i++] = "delete"; + permissions[i++] = NULL; + + version = xdp_dbus_documents_get_version (documents); + full_flags = DOCUMENT_ADD_FLAGS_REUSE_EXISTING | DOCUMENT_ADD_FLAGS_PERSISTENT | DOCUMENT_ADD_FLAGS_AS_NEEDED_BY_APP; + if (flags & DOCUMENT_FLAG_DIRECTORY) + full_flags |= DOCUMENT_ADD_FLAGS_DIRECTORY; + + if (flags & DOCUMENT_FLAG_FOR_SAVE) + { + if (version >= 3) + { + ret = xdp_dbus_documents_call_add_named_full_sync (documents, + g_variant_new_handle (fd_in), + basename, + full_flags, + app_id, + permissions, + fd_list, + &doc_id, + NULL, + NULL, + NULL, + error); + handled_permissions = TRUE; + } + else + ret = xdp_dbus_documents_call_add_named_sync (documents, + g_variant_new_handle (fd_in), + basename, + TRUE, + TRUE, + fd_list, + &doc_id, + NULL, + NULL, + error); + } + else + { + if (version >= 2) + { + ret = xdp_dbus_documents_call_add_full_sync (documents, + g_variant_new_fixed_array (G_VARIANT_TYPE_HANDLE, &fd_in, 1, sizeof (gint32)), + full_flags, + app_id, + permissions, + fd_list, + &doc_ids, + NULL, + NULL, + NULL, + error); + handled_permissions = TRUE; + } + else + ret = xdp_dbus_documents_call_add_sync (documents, + g_variant_new_handle (fd_in), + TRUE, + TRUE, + fd_list, + &doc_id, + NULL, + NULL, + error); + } + + g_object_unref (fd_list); + + if (!ret) + return NULL; + + if (doc_ids && doc_ids[0]) { + doc_id = g_strdup (doc_ids[0]); + } + + if (!handled_permissions) + { + if (!xdp_dbus_documents_call_grant_permissions_sync (documents, + doc_id, + app_id, + permissions, + NULL, + error)) + return NULL; + } + + if (!g_strcmp0 (doc_id, "")) + { + doc_path = g_build_filename (path, NULL); + return g_filename_to_uri (doc_path, NULL, NULL); + } + + doc_path = g_build_filename (documents_mountpoint, doc_id, basename, NULL); + return g_filename_to_uri (doc_path, NULL, NULL); +} + +char * +get_real_path_for_doc_path (const char *path, + XdpAppInfo *app_info) +{ + g_autofree char *doc_id = NULL; + gboolean ret = FALSE; + g_autoptr(GError) error = NULL; + + if (xdp_app_info_is_host (app_info)) + return g_strdup (path); + + ret = xdp_dbus_documents_call_lookup_sync (documents, path, &doc_id, NULL, &error); + if (!ret) + { + g_debug ("document portal error for path '%s': %s", path, error->message); + return g_strdup (path); + } + + if (!g_strcmp0 (doc_id, "")) + { + g_debug ("document portal returned empty doc id for path '%s'", path); + return g_strdup (path); + } + + return get_real_path_for_doc_id (doc_id); +} + +char * +get_real_path_for_doc_id (const char *doc_id) +{ + gboolean ret = FALSE; + char *real_path = NULL; + g_autoptr (GError) error = NULL; + + ret = xdp_dbus_documents_call_info_sync (documents, doc_id, &real_path, NULL, NULL, &error); + if (!ret) + { + g_debug ("document portal error for doc id '%s': %s", doc_id, error->message); + return NULL; + } + + return real_path; +} diff --git a/src/documents.h b/src/documents.h new file mode 100644 index 0000000..a5b53a7 --- /dev/null +++ b/src/documents.h @@ -0,0 +1,43 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +#pragma once + +#include + +typedef enum { + DOCUMENT_FLAG_NONE = 0, + DOCUMENT_FLAG_FOR_SAVE = (1 << 0), + DOCUMENT_FLAG_WRITABLE = (1 << 1), + DOCUMENT_FLAG_DIRECTORY = (1 << 2), + DOCUMENT_FLAG_DELETABLE = (1 << 3), +} DocumentFlags; + +void init_document_proxy (GDBusConnection *connection); + +char *register_document (const char *uri, + const char *app_id, + DocumentFlags flags, + GError **error); + +char *get_real_path_for_doc_path (const char *path, + XdpAppInfo *app_info); + +char *get_real_path_for_doc_id (const char *doc_id); diff --git a/src/dynamic-launcher.c b/src/dynamic-launcher.c new file mode 100644 index 0000000..556049e --- /dev/null +++ b/src/dynamic-launcher.c @@ -0,0 +1,1047 @@ +/* + * Copyright © 2022 Matthew Leeds + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthew Leeds + */ + +#include "config.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "dynamic-launcher.h" +#include "request.h" +#include "call.h" +#include "xdp-dbus.h" +#include "xdp-impl-dbus.h" +#include "xdp-utils.h" + +#define MAX_DESKTOP_SIZE_BYTES 1048576 + +typedef struct _DynamicLauncher DynamicLauncher; +typedef struct _DynamicLauncherClass DynamicLauncherClass; + +struct _DynamicLauncher +{ + XdpDbusDynamicLauncherSkeleton parent_instance; +}; + +struct _DynamicLauncherClass +{ + XdpDbusDynamicLauncherSkeletonClass parent_class; +}; + +static XdpDbusImplDynamicLauncher *impl; +static DynamicLauncher *dynamic_launcher; + +static GMutex transient_permissions_lock; +static GHashTable *transient_permissions; + +GType dynamic_launcher_get_type (void) G_GNUC_CONST; +static void dynamic_launcher_iface_init (XdpDbusDynamicLauncherIface *iface); + +G_DEFINE_TYPE_WITH_CODE (DynamicLauncher, dynamic_launcher, + XDP_DBUS_TYPE_DYNAMIC_LAUNCHER_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_DYNAMIC_LAUNCHER, + dynamic_launcher_iface_init)); + +typedef enum { + DYNAMIC_LAUNCHER_TYPE_APPLICATION = 1, + DYNAMIC_LAUNCHER_TYPE_WEBAPP = 2, +} DynamicLauncherType; + +static GVariant * +get_launcher_data_and_revoke_token (const char *token) +{ + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&transient_permissions_lock); + GVariant *launcher_data_wrapped; + + if (!transient_permissions) + return NULL; + + if (!g_uuid_string_is_valid (token)) + return NULL; + + launcher_data_wrapped = g_hash_table_lookup (transient_permissions, token); + if (launcher_data_wrapped) + { + g_autoptr(GVariant) launcher_data = NULL; + guint timeout_id; + + g_variant_get (launcher_data_wrapped, "(vu)", &launcher_data, &timeout_id); + + g_source_remove (timeout_id); + g_hash_table_remove (transient_permissions, token); + + return g_steal_pointer (&launcher_data); + } + + return NULL; +} + +static gboolean +validate_desktop_file_id (const char *app_id, + const char *desktop_file_id, + GError **error) +{ + const char *after_app_id; + + if (!g_str_has_suffix (desktop_file_id, ".desktop")) + { + g_set_error (error, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + _("Desktop file id missing .desktop suffix: %s"), desktop_file_id); + return FALSE; + } + + if (app_id == NULL || *app_id == '\0') + return TRUE; + + after_app_id = desktop_file_id + strlen (app_id); + if (!g_str_has_prefix (desktop_file_id, app_id) || *after_app_id != '.') + { + g_set_error (error, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + _("Desktop file id missing app id prefix '%s.': %s"), + app_id, desktop_file_id); + return FALSE; + } + + return TRUE; +} + +static gboolean +write_icon_to_disk (GVariant *icon_v, + const char *icon_subdir, + const char *icon_path, + GError **error) +{ + g_autoptr(GIcon) icon = NULL; + g_autoptr(GFile) icon_file = NULL; + g_autoptr(GFileOutputStream) icon_stream = NULL; + GBytes *icon_bytes; + gconstpointer bytes_data; + gsize bytes_len; + + icon = g_icon_deserialize (icon_v); + g_assert (G_IS_BYTES_ICON (icon)); + icon_bytes = g_bytes_icon_get_bytes (G_BYTES_ICON (icon)); + + g_mkdir_with_parents (icon_subdir, 0700); + icon_file = g_file_new_for_path (icon_path); + icon_stream = g_file_replace (icon_file, NULL, FALSE, G_FILE_CREATE_REPLACE_DESTINATION, NULL, error); + if (icon_stream == NULL) + return FALSE; + + /* Use write_all() instead of write_bytes() so we don't have to worry about + * partial writes (https://gitlab.gnome.org/GNOME/glib/-/issues/570). + */ + bytes_data = g_bytes_get_data (icon_bytes, &bytes_len); + if (!g_output_stream_write_all (G_OUTPUT_STREAM (icon_stream), + bytes_data, bytes_len, + NULL, NULL, error) || + !g_output_stream_close (G_OUTPUT_STREAM (icon_stream), NULL, error)) + return FALSE; + + return TRUE; +} + +static GKeyFile * +save_icon_and_get_desktop_entry (const char *desktop_file_id, + const char *desktop_entry, + GVariant *launcher_data, + XdpAppInfo *xdp_app_info, + char **out_icon_path, + GError **error) +{ + g_autoptr(GVariant) icon_v = NULL; + g_autoptr(GDesktopAppInfo) desktop_app_info = NULL; + g_autoptr(GKeyFile) key_file = g_key_file_new (); + g_autofree char *exec = NULL; + g_auto(GStrv) exec_strv = NULL; + g_auto(GStrv) prefixed_exec_strv = NULL; + g_auto(GStrv) groups = NULL; + g_autofree char *prefixed_exec = NULL; + g_autofree char *tryexec_path = NULL; + g_autofree char *icon_path = NULL; + g_autofree char *icon_subdir = NULL; + const char *name, *icon_extension, *icon_size; + const char *app_id; + + g_variant_get (launcher_data, "(&sv&s&s)", &name, &icon_v, &icon_extension, &icon_size); + g_assert (name != NULL && name[0] != '\0'); + g_assert (icon_v); + g_assert (icon_extension != NULL && icon_extension[0] != '\0'); + g_assert (icon_size != NULL && icon_size[0] != '\0'); + + app_id = xdp_app_info_get_id (xdp_app_info); + + if (!g_key_file_load_from_data (key_file, desktop_entry, -1, + G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS, + error)) + { + g_set_error (error, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + _("Desktop entry given to Install() not a valid key file")); + return NULL; + } + + /* The desktop entry spec supports more than one group but we don't in case + * there's a security risk. + */ + groups = g_key_file_get_groups (key_file, NULL); + if (g_strv_length (groups) > 1 || !g_strv_contains ((const char * const *)groups, G_KEY_FILE_DESKTOP_GROUP)) + { + g_set_error (error, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + _("Desktop entry given to Install() must have only one group")); + return NULL; + } + + /* Overwrite Name= and Icon= if they are present */ + g_key_file_set_string (key_file, G_KEY_FILE_DESKTOP_GROUP, "Name", name); + + { + g_autofree char *no_dot_desktop = NULL; + g_autofree char *icon_name = NULL; + g_autofree char *subdir = NULL; + + no_dot_desktop = g_strndup (desktop_file_id, strlen(desktop_file_id) - strlen (".desktop")); + icon_name = g_strconcat (no_dot_desktop, ".", icon_extension, NULL); + + /* Put the icon in a per-size subdirectory so the size is discernible + * without reading the file + */ + if (g_strcmp0 (icon_extension, "svg") == 0) + subdir = g_strdup ("scalable"); + else + subdir = g_strdup_printf ("%sx%s", icon_size, icon_size); + + icon_subdir = g_build_filename (g_get_user_data_dir (), XDG_PORTAL_ICONS_DIR, subdir, NULL); + icon_path = g_build_filename (icon_subdir, icon_name, NULL); + + g_key_file_set_string (key_file, G_KEY_FILE_DESKTOP_GROUP, "Icon", icon_path); + } + + exec = g_key_file_get_string (key_file, G_KEY_FILE_DESKTOP_GROUP, "Exec", error); + if (exec == NULL) + return NULL; + + if (!g_shell_parse_argv (exec, NULL, &exec_strv, error)) + return NULL; + + /* Don't let the app give itself access to host files */ + if (xdp_app_info_get_kind (xdp_app_info) == XDP_APP_INFO_KIND_FLATPAK && + g_strv_contains ((const char * const *)exec_strv, "--file-forwarding")) + { + g_set_error (error, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + _("Desktop entry given to Install() must not use --file-forwarding")); + return NULL; + } + + prefixed_exec_strv = xdp_app_info_rewrite_commandline (xdp_app_info, + (const char * const *)exec_strv, + TRUE /* quote escape */); + if (prefixed_exec_strv == NULL) + { + g_set_error (error, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_FAILED, + _("DynamicLauncher install not supported for: %s"), app_id); + return NULL; + } + + prefixed_exec = g_strjoinv (" ", prefixed_exec_strv); + g_key_file_set_value (key_file, G_KEY_FILE_DESKTOP_GROUP, "Exec", prefixed_exec); + + tryexec_path = xdp_app_info_get_tryexec_path (xdp_app_info); + if (tryexec_path != NULL) + g_key_file_set_value (key_file, G_KEY_FILE_DESKTOP_GROUP, "TryExec", tryexec_path); + + if (xdp_app_info_get_kind (xdp_app_info) == XDP_APP_INFO_KIND_FLATPAK) + { + /* Flatpak checks for this key */ + g_key_file_set_value (key_file, G_KEY_FILE_DESKTOP_GROUP, "X-Flatpak", app_id); + /* Flatpak removes this one for security */ + g_key_file_remove_key (key_file, G_KEY_FILE_DESKTOP_GROUP, "X-GNOME-Bugzilla-ExtraInfoScript", NULL); + } + + desktop_app_info = g_desktop_app_info_new_from_keyfile (key_file); + if (desktop_app_info == NULL) + { + g_set_error (error, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + _("Desktop entry given to Install() not valid")); + return NULL; + } + + /* Write the icon last so it's only on-disk if other checks passed */ + if (!write_icon_to_disk (icon_v, icon_subdir, icon_path, error)) + return NULL; + + if (out_icon_path) + *out_icon_path = g_steal_pointer (&icon_path); + + return g_steal_pointer (&key_file); +} + +static gboolean +handle_install (XdpDbusDynamicLauncher *object, + GDBusMethodInvocation *invocation, + const gchar *arg_token, + const gchar *arg_desktop_file_id, + const gchar *arg_desktop_entry, + GVariant *arg_options) +{ + Call *call = call_from_invocation (invocation); + const char *app_id = xdp_app_info_get_id (call->app_info); + g_autoptr(GVariant) launcher_data = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GKeyFile) desktop_keyfile = NULL; + g_autofree char *icon_path = NULL; + g_autofree char *desktop_dir = NULL; + g_autofree char *desktop_path = NULL; + g_autofree char *link_path = NULL; + g_autofree char *relative_path = NULL; + g_autoptr(GFile) link_file = NULL; + gsize desktop_entry_length = G_MAXSIZE; + + launcher_data = get_launcher_data_and_revoke_token (arg_token); + if (launcher_data == NULL) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + _("Token given is invalid: %s"), arg_token); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + + if (!validate_desktop_file_id (app_id, arg_desktop_file_id, &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + desktop_keyfile = save_icon_and_get_desktop_entry (arg_desktop_file_id, + arg_desktop_entry, + launcher_data, + call->app_info, + &icon_path, + &error); + if (desktop_keyfile == NULL) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_free (g_key_file_to_data (desktop_keyfile, &desktop_entry_length, NULL)); + if (desktop_entry_length > MAX_DESKTOP_SIZE_BYTES) + { + g_set_error (&error, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_FAILED, + _("Desktop file exceeds max size (%d): %s"), + MAX_DESKTOP_SIZE_BYTES, arg_desktop_file_id); + goto error; + } + + /* Put the desktop file in ~/.local/share/xdg-desktop-portal/applications/ so + * there's no ambiguity about which launchers were created by this portal. + */ + desktop_dir = g_build_filename (g_get_user_data_dir (), XDG_PORTAL_APPLICATIONS_DIR, NULL); + g_mkdir_with_parents (desktop_dir, 0700); + desktop_path = g_build_filename (desktop_dir, arg_desktop_file_id, NULL); + if (!g_key_file_save_to_file (desktop_keyfile, desktop_path, &error)) + goto error; + + /* Make a sym link in ~/.local/share/applications so the launcher shows up in + * the desktop environment's menu. + */ + link_path = g_build_filename (g_get_user_data_dir (), "applications", arg_desktop_file_id, NULL); + link_file = g_file_new_for_path (link_path); + relative_path = g_build_filename ("..", XDG_PORTAL_APPLICATIONS_DIR, arg_desktop_file_id, NULL); + g_file_delete (link_file, NULL, NULL); + if (!g_file_make_symbolic_link (link_file, relative_path, NULL, &error)) + goto error; + + xdp_dbus_dynamic_launcher_complete_install (object, invocation); + return G_DBUS_METHOD_INVOCATION_HANDLED; + +error: + g_dbus_method_invocation_return_gerror (invocation, error); + remove (icon_path); + remove (desktop_path); + remove (link_path); + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static XdpOptionKey response_options[] = { + { "name", G_VARIANT_TYPE_STRING, NULL }, + { "icon", G_VARIANT_TYPE_VARIANT, NULL }, + { "token", G_VARIANT_TYPE_UINT32, NULL } +}; + +static gboolean +install_token_timeout (gpointer data) +{ + g_autoptr(GVariant) launcher_data = NULL; + + g_debug ("Revoking install token %s", (char *)data); + launcher_data = get_launcher_data_and_revoke_token ((char *)data); + g_free (data); + + return G_SOURCE_REMOVE; +} + +static void +set_launcher_data_for_token (const char *token, + GVariant *launcher_data) +{ + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&transient_permissions_lock); + guint timeout_id; + GVariant *launcher_data_wrapped; + + if (!transient_permissions) + { + transient_permissions = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, (GDestroyNotify)g_variant_unref); + } + + /* Revoke the token if it hasn't been used after 5 minutes, in case of + * client bugs. This is what the GNOME print portal implementation does. + */ + timeout_id = g_timeout_add_seconds_full (G_PRIORITY_DEFAULT, 300, install_token_timeout, + g_strdup (token), g_free); + launcher_data_wrapped = g_variant_new ("(vu)", launcher_data, timeout_id); + + g_hash_table_insert (transient_permissions, + g_strdup (token), + g_variant_ref_sink (launcher_data_wrapped)); +} + +static void +prepare_install_done (GObject *source, + GAsyncResult *result, + gpointer data) +{ + g_autoptr(Request) request = data; + GVariant *launcher_data; + guint response = 2; + g_autoptr(GVariant) results = NULL; + g_autoptr(GError) error = NULL; + GVariantBuilder results_builder; + + REQUEST_AUTOLOCK (request); + + g_variant_builder_init (&results_builder, G_VARIANT_TYPE_VARDICT); + + if (!xdp_dbus_impl_dynamic_launcher_call_prepare_install_finish (XDP_DBUS_IMPL_DYNAMIC_LAUNCHER (source), + &response, + &results, + result, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("Backend call failed: %s", error->message); + goto out; + } + + if (request->exported && response == 0) + { + g_autofree char *token = g_uuid_string_random (); + const char *chosen_name = NULL; + const char *icon_format = NULL; + const char *icon_size = NULL; + GVariant *chosen_icon = NULL; + + icon_format = g_object_get_data (G_OBJECT (request), "icon-format"); + g_assert (icon_format != NULL && icon_format[0] != '\0'); + icon_size = g_object_get_data (G_OBJECT (request), "icon-size"); + g_assert (icon_size != NULL && icon_size[0] != '\0'); + + if (!xdp_filter_options (results, &results_builder, + response_options, G_N_ELEMENTS (response_options), + &error) || + !g_variant_lookup (results, "name", "&s", &chosen_name) || + chosen_name[0] == '\0' || + !g_variant_lookup (results, "icon", "v", &chosen_icon)) + { + g_warning ("Results from backend failed validation: %s", + error ? error->message : "missing entries"); + response = 2; + } + else + { + /* Save the token in memory and return it to the caller */ + launcher_data = g_variant_new ("(svss)", chosen_name, chosen_icon, icon_format, icon_size); + set_launcher_data_for_token (token, launcher_data); + g_variant_builder_add (&results_builder, "{sv}", "token", g_variant_new_string (token)); + } + } + +out: + if (request->exported) + { + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + response, + g_variant_builder_end (&results_builder)); + + request_unexport (request); + } + else + { + g_variant_builder_clear (&results_builder); + } +} + +static gboolean +validate_url (const char *key, + GVariant *value, + GVariant *options, + GError **error) +{ + const char *url = g_variant_get_string (value, NULL); + g_autoptr(GError) local_error = NULL; + guint32 launcher_type; + + g_variant_lookup (options, "launcher_type", "u", &launcher_type); + if (launcher_type == DYNAMIC_LAUNCHER_TYPE_WEBAPP && + !g_uri_is_valid (url, G_URI_FLAGS_NONE, &local_error)) + { + g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + _("URL given is invalid: %s"), local_error->message); + return FALSE; + } + + return TRUE; +} + +static gboolean +validate_launcher_type (const char *key, + GVariant *value, + GVariant *options, + GError **error) +{ + guint32 launcher_type = g_variant_get_uint32 (value); + guint32 supported_launcher_types; + + supported_launcher_types = + xdp_dbus_dynamic_launcher_get_supported_launcher_types + (XDP_DBUS_DYNAMIC_LAUNCHER (dynamic_launcher)); + + if (__builtin_popcount (launcher_type) != 1) + { + g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + _("Invalid launcher type: %x"), launcher_type); + return FALSE; + } + + if (!(supported_launcher_types & launcher_type)) + { + g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + _("Unsupported launcher type: %x"), launcher_type); + return FALSE; + } + + return TRUE; +} + +static XdpOptionKey prepare_install_options[] = { + { "modal", G_VARIANT_TYPE_BOOLEAN }, + { "launcher_type", G_VARIANT_TYPE_UINT32, validate_launcher_type }, + { "target", G_VARIANT_TYPE_STRING, validate_url }, + { "editable_name", G_VARIANT_TYPE_BOOLEAN }, + { "editable_icon", G_VARIANT_TYPE_BOOLEAN } +}; + +static gboolean +handle_prepare_install (XdpDbusDynamicLauncher *object, + GDBusMethodInvocation *invocation, + const gchar *arg_parent_window, + const gchar *arg_name, + GVariant *arg_icon_v, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + const char *app_id = xdp_app_info_get_id (request->app_info); + g_autoptr(GError) error = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + GVariantBuilder opt_builder; + g_autofree char *icon_format = NULL; + g_autofree char *icon_size = NULL; + g_autoptr(GVariant) icon_v = NULL; + + REQUEST_AUTOLOCK (request); + + impl_request = xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, &error); + if (!impl_request) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request (request, impl_request); + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &opt_builder, + prepare_install_options, G_N_ELEMENTS (prepare_install_options), &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + /* Do some validation on the icon before passing it along */ + icon_v = g_variant_get_variant (arg_icon_v); + if (!icon_v || !xdp_validate_serialized_icon (icon_v, TRUE /* bytes_only */, + &icon_format, &icon_size)) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + _("Dynamic launcher icon failed validation")); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_object_set_data_full (G_OBJECT (request), "icon-format", g_steal_pointer (&icon_format), g_free); + g_object_set_data_full (G_OBJECT (request), "icon-size", g_steal_pointer (&icon_size), g_free); + + xdp_dbus_impl_dynamic_launcher_call_prepare_install (impl, + request->id, + app_id, + arg_parent_window, + arg_name, + arg_icon_v, + g_variant_builder_end (&opt_builder), + NULL, /* cancellable */ + prepare_install_done, + g_object_ref (request)); + + xdp_dbus_dynamic_launcher_complete_prepare_install (object, invocation, request->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_request_install_token (XdpDbusDynamicLauncher *object, + GDBusMethodInvocation *invocation, + const gchar *arg_name, + GVariant *arg_icon_v, + GVariant *arg_options) +{ + Call *call = call_from_invocation (invocation); + const char *app_id = xdp_app_info_get_id (call->app_info); + g_autoptr(GError) error = NULL; + GVariant *launcher_data; + g_autofree char *token = NULL; + g_autofree char *icon_format = NULL; + g_autofree char *icon_size = NULL; + g_autoptr(GVariant) icon_v = NULL; + guint response = 2; + + /* Don't enforce app ID requirements on unsandboxed apps if the app ID + * couldn't be determined. Otherwise the check would fail if for example the + * app was launched from the CLI: + * https://github.com/flatpak/xdg-desktop-portal/pull/719#issuecomment-1057412221 + */ + if (xdp_app_info_is_host (call->app_info) && g_str_equal (app_id, "")) + { + response = 0; + } + else if (!xdp_dbus_impl_dynamic_launcher_call_request_install_token_sync (impl, + app_id, + arg_options, + &response, + NULL, /* cancellable */ + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("Backend call failed: %s", error->message); + response = 2; + } + + if (response == 0) + { + /* Do some validation on the icon before saving it */ + icon_v = g_variant_get_variant (arg_icon_v); + if (!icon_v || !xdp_validate_serialized_icon (icon_v, TRUE /* bytes_only */, + &icon_format, &icon_size)) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + _("Dynamic launcher icon failed validation")); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + launcher_data = g_variant_new ("(svss)", arg_name, icon_v, icon_format, icon_size); + token = g_uuid_string_random (); + + /* Save the token in memory and return it to the caller */ + set_launcher_data_for_token (token, launcher_data); + + xdp_dbus_dynamic_launcher_complete_request_install_token (object, invocation, token); + } + else + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + _("RequestInstallToken() not allowed for app id %s"), + xdp_app_info_get_id (call->app_info)); + } + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_uninstall (XdpDbusDynamicLauncher *object, + GDBusMethodInvocation *invocation, + const gchar *arg_desktop_file_id, + GVariant *arg_options) +{ + Call *call = call_from_invocation (invocation); + const char *app_id = xdp_app_info_get_id (call->app_info); + g_autoptr(GError) error = NULL; + g_autoptr(GError) desktop_file_error = NULL; + g_autofree char *icon_dir = NULL; + g_autofree char *icon_path = NULL; + g_autofree char *desktop_dir = NULL; + g_autoptr(GFile) icon_file = NULL; + g_autoptr(GFile) desktop_file = NULL; + g_autoptr(GFile) link_file = NULL; + g_autoptr(GKeyFile) desktop_keyfile = NULL; + + if (!validate_desktop_file_id (app_id, arg_desktop_file_id, &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + icon_dir = g_build_filename (g_get_user_data_dir (), XDG_PORTAL_ICONS_DIR, NULL); + desktop_dir = g_build_filename (g_get_user_data_dir (), XDG_PORTAL_APPLICATIONS_DIR, NULL); + + link_file = g_file_new_build_filename (g_get_user_data_dir (), "applications", arg_desktop_file_id, NULL); + if (!g_file_delete (link_file, NULL, &error)) + { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + g_info ("Uninstall() method failed because launcher '%s' does not exist", arg_desktop_file_id); + goto error; + } + + desktop_file = g_file_new_build_filename (desktop_dir, arg_desktop_file_id, NULL); + desktop_keyfile = g_key_file_new (); + if (g_key_file_load_from_file (desktop_keyfile, g_file_peek_path (desktop_file), G_KEY_FILE_NONE, NULL)) + icon_path = g_key_file_get_string (desktop_keyfile, G_KEY_FILE_DESKTOP_GROUP, "Icon", NULL); + + g_file_delete (desktop_file, NULL, &desktop_file_error); + + if (icon_path && g_str_has_prefix (icon_path, icon_dir)) + { + icon_file = g_file_new_for_path (icon_path); + if (!g_file_delete (icon_file, NULL, &error)) + goto error; + } + + if (desktop_file_error) + goto error; + + xdp_dbus_dynamic_launcher_complete_uninstall (object, invocation); + return G_DBUS_METHOD_INVOCATION_HANDLED; + +error: + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_get_desktop_entry (XdpDbusDynamicLauncher *object, + GDBusMethodInvocation *invocation, + const gchar *arg_desktop_file_id) +{ + Call *call = call_from_invocation (invocation); + const char *app_id = xdp_app_info_get_id (call->app_info); + g_autoptr(GError) error = NULL; + g_autofree char *desktop_dir = NULL; + g_autofree char *contents = NULL; + g_autofree char *desktop_path = NULL; + gsize length; + + if (!validate_desktop_file_id (app_id, arg_desktop_file_id, &error)) + goto error; + + desktop_dir = g_build_filename (g_get_user_data_dir (), XDG_PORTAL_APPLICATIONS_DIR, NULL); + + desktop_path = g_build_filename (desktop_dir, arg_desktop_file_id, NULL); + if (!g_file_get_contents (desktop_path, &contents, &length, &error)) + goto error; + if (length > MAX_DESKTOP_SIZE_BYTES) + { + g_set_error (&error, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_FAILED, + _("Desktop file exceeds max size (%d): %s"), + MAX_DESKTOP_SIZE_BYTES, arg_desktop_file_id); + goto error; + } + + xdp_dbus_dynamic_launcher_complete_get_desktop_entry (object, invocation, contents); + return G_DBUS_METHOD_INVOCATION_HANDLED; + +error: + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_get_icon (XdpDbusDynamicLauncher *object, + GDBusMethodInvocation *invocation, + const gchar *arg_desktop_file_id) +{ + Call *call = call_from_invocation (invocation); + const char *app_id = xdp_app_info_get_id (call->app_info); + g_autoptr(GError) error = NULL; + g_autofree char *desktop_dir = NULL; + g_autofree char *contents = NULL; + g_autofree char *desktop_path = NULL; + g_autofree char *icon_dir = NULL; + g_autofree char *icon_path = NULL; + gsize length; + g_autoptr(GKeyFile) key_file = NULL; + g_autoptr(GFile) icon_file = NULL; + g_autoptr(GIcon) icon = NULL; + g_autoptr(GInputStream) stream = NULL; + g_autoptr(GBytes) bytes = NULL; + g_autoptr(GIcon) bytes_icon = NULL; + g_autoptr(GVariant) icon_v = NULL; + const gchar *icon_format = NULL; + int icon_size = 0; + + if (!validate_desktop_file_id (app_id, arg_desktop_file_id, &error)) + goto error; + + desktop_dir = g_build_filename (g_get_user_data_dir (), XDG_PORTAL_APPLICATIONS_DIR, NULL); + icon_dir = g_build_filename (g_get_user_data_dir (), XDG_PORTAL_ICONS_DIR, NULL); + + desktop_path = g_build_filename (desktop_dir, arg_desktop_file_id, NULL); + if (!g_file_get_contents (desktop_path, &contents, &length, &error)) + goto error; + if (length > MAX_DESKTOP_SIZE_BYTES) + { + g_set_error (&error, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_FAILED, + _("Desktop file exceeds max size (%d): %s"), + MAX_DESKTOP_SIZE_BYTES, arg_desktop_file_id); + goto error; + } + + key_file = g_key_file_new (); + if (!g_key_file_load_from_data (key_file, contents, -1, G_KEY_FILE_NONE, &error)) + goto error; + + icon_path = g_key_file_get_string (key_file, G_KEY_FILE_DESKTOP_GROUP, "Icon", NULL); + if (icon_path && g_str_has_prefix (icon_path, icon_dir)) + { + g_autofree char *icon_dir = NULL; + g_autofree char *icon_dir_basename = NULL; + const char *x; + + if (g_str_has_suffix (icon_path, ".png")) + icon_format = "png"; + else if (g_str_has_suffix (icon_path, ".svg")) + icon_format = "svg"; + else if (g_str_has_suffix (icon_path, ".jpeg") || g_str_has_suffix (icon_path, ".jpg")) + icon_format = "jpeg"; + + /* dir should be either scalable or e.g. 512x512 */ + icon_dir = g_path_get_dirname (icon_path); + icon_dir_basename = g_path_get_basename (icon_dir); + if (g_strcmp0 (icon_dir_basename, "scalable") == 0) { + /* An svg can have a width and height set, but it is probably not + * needed since it can be scaled to any size. + */ + icon_size = 4096; + } else if ((x = strchr (icon_dir_basename, 'x')) != NULL) { + icon_size = atoi (x + 1); + } + } + + if (!icon_format || icon_size <= 0 || icon_size > 4096) + { + g_set_error (&error, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_FAILED, + _("Desktop file '%s' icon at unrecognized path"), arg_desktop_file_id); + goto error; + } + + icon_file = g_file_new_for_path (icon_path); + icon = g_file_icon_new (icon_file); + stream = g_loadable_icon_load (G_LOADABLE_ICON (icon), 0, NULL, NULL, NULL); + + /* Icons are usually smaller than 1 MiB. Set a 10 MiB + * limit so we can't use a huge amount of memory or hit + * the D-Bus message size limit + */ + if (stream) + bytes = g_input_stream_read_bytes (stream, 10485760 /* 10 MiB */, NULL, NULL); + if (bytes) + bytes_icon = g_bytes_icon_new (bytes); + if (bytes_icon) + icon_v = g_icon_serialize (bytes_icon); + + if (icon_v == NULL) + { + g_set_error (&error, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_FAILED, + _("Desktop file '%s' icon failed to serialize"), arg_desktop_file_id); + goto error; + } + + xdp_dbus_dynamic_launcher_complete_get_icon (object, invocation, + g_variant_new_variant (icon_v), + icon_format, icon_size); + return G_DBUS_METHOD_INVOCATION_HANDLED; + +error: + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_launch (XdpDbusDynamicLauncher *object, + GDBusMethodInvocation *invocation, + const gchar *arg_desktop_file_id, + GVariant *arg_options) +{ + Call *call = call_from_invocation (invocation); + const char *app_id = xdp_app_info_get_id (call->app_info); + g_autoptr(GError) error = NULL; + g_autofree char *desktop_dir = NULL; + g_autofree char *desktop_path = NULL; + const char *activation_token = NULL; + g_autoptr(GAppLaunchContext) launch_context = NULL; + g_autoptr(GDesktopAppInfo) app_info = NULL; + + if (!validate_desktop_file_id (app_id, arg_desktop_file_id, &error)) + goto error; + + desktop_dir = g_build_filename (g_get_user_data_dir (), XDG_PORTAL_APPLICATIONS_DIR, NULL); + + desktop_path = g_build_filename (desktop_dir, arg_desktop_file_id, NULL); + if (!g_file_test (desktop_path, G_FILE_TEST_EXISTS)) + { + g_set_error (&error, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + _("No dynamic launcher exists with id '%s'"), arg_desktop_file_id); + goto error; + } + + /* Unset env var set in main() */ + launch_context = g_app_launch_context_new (); + g_app_launch_context_unsetenv (launch_context, "GIO_USE_VFS"); + + /* Set activation token for focus stealing prevention */ + g_variant_lookup (arg_options, "activation_token", "&s", &activation_token); + if (activation_token) + g_app_launch_context_setenv (launch_context, "XDG_ACTIVATION_TOKEN", activation_token); + + app_info = g_desktop_app_info_new_from_filename (desktop_path); + if (app_info == NULL) + { + g_set_error (&error, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_FAILED, + _("Failed to create GDesktopAppInfo for launcher with id '%s'"), + arg_desktop_file_id); + goto error; + } + + g_debug ("Launching %s", arg_desktop_file_id); + if (!g_app_info_launch (G_APP_INFO (app_info), NULL, launch_context, &error)) + goto error; + + xdp_dbus_dynamic_launcher_complete_launch (object, invocation); + return G_DBUS_METHOD_INVOCATION_HANDLED; + +error: + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +dynamic_launcher_iface_init (XdpDbusDynamicLauncherIface *iface) +{ + iface->handle_install = handle_install; + iface->handle_prepare_install = handle_prepare_install; + iface->handle_request_install_token = handle_request_install_token; + iface->handle_uninstall = handle_uninstall; + iface->handle_get_desktop_entry = handle_get_desktop_entry; + iface->handle_get_icon = handle_get_icon; + iface->handle_launch = handle_launch; +} + +static void +dynamic_launcher_init (DynamicLauncher *dl) +{ + xdp_dbus_dynamic_launcher_set_version (XDP_DBUS_DYNAMIC_LAUNCHER (dl), 1); + g_object_bind_property (G_OBJECT (impl), "supported-launcher-types", + G_OBJECT (dl), "supported-launcher-types", + G_BINDING_SYNC_CREATE); +} + +static void +dynamic_launcher_class_init (DynamicLauncherClass *klass) +{ +} + +GDBusInterfaceSkeleton * +dynamic_launcher_create (GDBusConnection *connection, + const char *dbus_name) +{ + g_autoptr(GError) error = NULL; + + impl = xdp_dbus_impl_dynamic_launcher_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_NONE, + dbus_name, + DESKTOP_PORTAL_OBJECT_PATH, + NULL, + &error); + if (impl == NULL) + { + g_warning ("Failed to create dynamic_launcher proxy: %s", error->message); + return NULL; + } + + g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (impl), G_MAXINT); + + dynamic_launcher = g_object_new (dynamic_launcher_get_type (), NULL); + + return G_DBUS_INTERFACE_SKELETON (dynamic_launcher); +} diff --git a/src/dynamic-launcher.h b/src/dynamic-launcher.h new file mode 100644 index 0000000..f95089d --- /dev/null +++ b/src/dynamic-launcher.h @@ -0,0 +1,31 @@ +/* + * Copyright © 2022 Matthew Leeds + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthew Leeds + */ + +#pragma once + +#include "config.h" + +#include + +#define XDG_PORTAL_APPLICATIONS_DIR "xdg-desktop-portal" G_DIR_SEPARATOR_S "applications" +#define XDG_PORTAL_ICONS_DIR "xdg-desktop-portal" G_DIR_SEPARATOR_S "icons" + +GDBusInterfaceSkeleton * dynamic_launcher_create (GDBusConnection *connection, + const char *dbus_name); diff --git a/src/email.c b/src/email.c new file mode 100644 index 0000000..403da4e --- /dev/null +++ b/src/email.c @@ -0,0 +1,336 @@ +/* + * Copyright © 2017 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +#include "config.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include "email.h" +#include "request.h" +#include "documents.h" +#include "xdp-dbus.h" +#include "xdp-impl-dbus.h" +#include "xdp-utils.h" + +typedef struct _Email Email; +typedef struct _EmailClass EmailClass; + +struct _Email +{ + XdpDbusEmailSkeleton parent_instance; +}; + +struct _EmailClass +{ + XdpDbusEmailSkeletonClass parent_class; +}; + +static XdpDbusImplEmail *impl; +static Email *email; + +GType email_get_type (void) G_GNUC_CONST; +static void email_iface_init (XdpDbusEmailIface *iface); + +G_DEFINE_TYPE_WITH_CODE (Email, email, XDP_DBUS_TYPE_EMAIL_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_EMAIL, + email_iface_init)); + +static void +send_response_in_thread_func (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + Request *request = task_data; + guint response; + GVariantBuilder new_results; + + g_variant_builder_init (&new_results, G_VARIANT_TYPE_VARDICT); + + REQUEST_AUTOLOCK (request); + + response = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (request), "response")); + + if (request->exported) + { + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + response, + g_variant_builder_end (&new_results)); + request_unexport (request); + } +} + +static void +compose_email_done (GObject *source, + GAsyncResult *result, + gpointer data) +{ + g_autoptr(Request) request = data; + guint response = 2; + g_autoptr(GVariant) results = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GTask) task = NULL; + + if (!xdp_dbus_impl_email_call_compose_email_finish (XDP_DBUS_IMPL_EMAIL (source), + &response, + &results, + result, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("Backend call failed: %s", error->message); + } + + g_object_set_data (G_OBJECT (request), "response", GINT_TO_POINTER (response)); + + task = g_task_new (NULL, NULL, NULL, NULL); + g_task_set_task_data (task, g_object_ref (request), g_object_unref); + g_task_run_in_thread (task, send_response_in_thread_func); +} + +static gboolean +is_valid_email (const char *string) +{ + return g_regex_match_simple ("^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$", string, 0, 0); +} + +static gboolean +validate_email_address (const char *key, + GVariant *value, + GVariant *options, + GError **error) +{ + const char *string = g_variant_get_string (value, NULL); + + if (!is_valid_email (string)) + { + g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "'%s' does not look like an email address", string); + return FALSE; + } + + return TRUE; +} + +static gboolean +validate_email_addresses (const char *key, + GVariant *value, + GVariant *options, + GError **error) +{ + const char *const *strings = g_variant_get_strv (value, NULL); + int i; + + for (i = 0; strings[i]; i++) + { + if (!is_valid_email (strings[i])) + { + g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "'%s' does not look like an email address", strings[i]); + return FALSE; + } + } + + return TRUE; +} + +static gboolean +validate_email_subject (const char *key, + GVariant *value, + GVariant *options, + GError **error) +{ + const char *string = g_variant_get_string (value, NULL); + + if (strchr (string, '\n')) + { + g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Not accepting multi-line subjects"); + return FALSE; + } + + if (g_utf8_strlen (string, -1) > 200) + { + g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Not accepting extremely long subjects"); + return FALSE; + } + + return TRUE; +} + +static XdpOptionKey compose_email_options[] = { + { "address", G_VARIANT_TYPE_STRING, validate_email_address }, + { "addresses", G_VARIANT_TYPE_STRING_ARRAY, validate_email_addresses }, + { "cc", G_VARIANT_TYPE_STRING_ARRAY, validate_email_addresses }, + { "bcc", G_VARIANT_TYPE_STRING_ARRAY, validate_email_addresses }, + { "subject", G_VARIANT_TYPE_STRING, validate_email_subject }, + { "body", G_VARIANT_TYPE_STRING, NULL } +}; + +static gboolean +handle_compose_email (XdpDbusEmail *object, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + const gchar *arg_parent_window, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + const char *app_id = xdp_app_info_get_id (request->app_info); + g_autoptr(GError) error = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + GVariantBuilder options; + g_autoptr(GVariant) attachment_fds = NULL; + + g_debug ("Handling ComposeEmail"); + + REQUEST_AUTOLOCK (request); + + impl_request = xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, &error); + if (!impl_request) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT); + + attachment_fds = g_variant_lookup_value (arg_options, "attachment_fds", G_VARIANT_TYPE ("ah")); + if (attachment_fds) + { + GVariantBuilder attachments; + int i; + + g_variant_builder_init (&attachments, G_VARIANT_TYPE_STRING_ARRAY); + for (i = 0; i < g_variant_n_children (attachment_fds); i++) + { + g_autofree char *path = NULL; + int fd_id; + int fd; + + g_variant_get_child (attachment_fds, i, "h", &fd_id); + fd = g_unix_fd_list_get (fd_list, fd_id, &error); + if (fd == -1) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + path = xdp_app_info_get_path_for_fd (request->app_info, fd, 0, NULL, NULL, &error); + + if (path == NULL) + { + g_debug ("Invalid attachment fd passed: %s", error->message); + + /* Don't leak any info about real file path existence, etc */ + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Invalid attachment fd passed"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_add (&attachments, "s", path); + } + + g_variant_builder_add (&options, "{sv}", "attachments", g_variant_builder_end (&attachments)); + } + + if (!xdp_filter_options (arg_options, &options, + compose_email_options, G_N_ELEMENTS (compose_email_options), + &error)) + { + g_debug ("Returning an error from option filtering"); + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request (request, impl_request); + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + xdp_dbus_email_complete_compose_email (object, invocation, NULL, request->id); + + xdp_dbus_impl_email_call_compose_email (impl, + request->id, + app_id, + arg_parent_window, + g_variant_builder_end (&options), + NULL, + compose_email_done, + g_object_ref (request)); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +email_iface_init (XdpDbusEmailIface *iface) +{ + iface->handle_compose_email = handle_compose_email; +} + +static void +email_init (Email *email) +{ + xdp_dbus_email_set_version (XDP_DBUS_EMAIL (email), 3); +} + +static void +email_class_init (EmailClass *klass) +{ +} + +GDBusInterfaceSkeleton * +email_create (GDBusConnection *connection, + const char *dbus_name) +{ + g_autoptr(GError) error = NULL; + + impl = xdp_dbus_impl_email_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_NONE, + dbus_name, + DESKTOP_PORTAL_OBJECT_PATH, + NULL, + &error); + + if (impl == NULL) + { + g_warning ("Failed to create email proxy: %s", error->message); + return NULL; + } + + g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (impl), G_MAXINT); + + email = g_object_new (email_get_type (), NULL); + + return G_DBUS_INTERFACE_SKELETON (email); +} diff --git a/src/email.h b/src/email.h new file mode 100644 index 0000000..16c0c02 --- /dev/null +++ b/src/email.h @@ -0,0 +1,26 @@ +/* + * Copyright © 2017 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +#pragma once + +#include + +GDBusInterfaceSkeleton * email_create (GDBusConnection *connection, + const char *dbus_name); diff --git a/src/file-chooser.c b/src/file-chooser.c new file mode 100644 index 0000000..c023eb1 --- /dev/null +++ b/src/file-chooser.c @@ -0,0 +1,911 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Alexander Larsson + * Matthias Clasen + */ + +#include "config.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include "file-chooser.h" +#include "request.h" +#include "documents.h" +#include "xdp-dbus.h" +#include "xdp-impl-dbus.h" +#include "xdp-utils.h" + +typedef struct _FileChooser FileChooser; +typedef struct _FileChooserClass FileChooserClass; + +struct _FileChooser +{ + XdpDbusFileChooserSkeleton parent_instance; +}; + +struct _FileChooserClass +{ + XdpDbusFileChooserSkeletonClass parent_class; +}; + +static XdpDbusImplLockdown *lockdown; +static XdpDbusImplFileChooser *impl; +static FileChooser *file_chooser; + +GType file_chooser_get_type (void) G_GNUC_CONST; +static void file_chooser_iface_init (XdpDbusFileChooserIface *iface); + +G_DEFINE_TYPE_WITH_CODE (FileChooser, file_chooser, + XDP_DBUS_TYPE_FILE_CHOOSER_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_FILE_CHOOSER, + file_chooser_iface_init)); + +static void +send_response_in_thread_func (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + Request *request = task_data; + GVariantBuilder results; + GVariantBuilder ruris; + guint response; + GVariant *options; + DocumentFlags flags = DOCUMENT_FLAG_WRITABLE | DOCUMENT_FLAG_DIRECTORY; + const char **uris; + GVariant *choices; + GVariant *current_filter; + GVariant *writable; + + g_variant_builder_init (&results, G_VARIANT_TYPE_VARDICT); + g_variant_builder_init (&ruris, G_VARIANT_TYPE_STRING_ARRAY); + + REQUEST_AUTOLOCK (request); + + if (GPOINTER_TO_INT (g_object_get_data (G_OBJECT (request), "for-save")) == TRUE) + flags |= DOCUMENT_FLAG_FOR_SAVE; + if (GPOINTER_TO_INT (g_object_get_data (G_OBJECT (request), "directory")) == FALSE) + flags &= ~DOCUMENT_FLAG_DIRECTORY; + response = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (request), "response")); + options = (GVariant *)g_object_get_data (G_OBJECT (request), "options"); + + if (response != 0) + goto out; + + writable = g_variant_lookup_value (options, "writable", G_VARIANT_TYPE("b")); + if (writable && !g_variant_get_boolean (writable)) + flags &= ~DOCUMENT_FLAG_WRITABLE; + + choices = g_variant_lookup_value (options, "choices", G_VARIANT_TYPE ("a(ss)")); + if (choices) + g_variant_builder_add (&results, "{sv}", "choices", choices); + + current_filter = g_variant_lookup_value (options, "current_filter", G_VARIANT_TYPE ("(sa(us))")); + if (current_filter) + g_variant_builder_add (&results, "{sv}", "current_filter", current_filter); + + if (g_variant_lookup (options, "uris", "^a&s", &uris)) + { + int i; + + for (i = 0; uris && uris[i]; i++) + { + g_autofree char *ruri = NULL; + g_autoptr(GError) error = NULL; + + if (xdp_app_info_is_host (request->app_info)) + ruri = g_strdup (uris[i]); + else + ruri = register_document (uris[i], xdp_app_info_get_id (request->app_info), flags, &error); + + if (ruri == NULL) + { + g_warning ("Failed to register %s: %s", uris[i], error->message); + continue; + } + g_debug ("convert uri %s -> %s\n", uris[i], ruri); + g_variant_builder_add (&ruris, "s", ruri); + } + } + +out: + g_variant_builder_add (&results, "{sv}", "uris", g_variant_builder_end (&ruris)); + + if (request->exported) + { + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + response, + g_variant_builder_end (&results)); + request_unexport (request); + } +} + +/* Calling Lookup on a nonexisting path does not work, so we + * pull the doc id out of the path manually. + */ +static gboolean +looks_like_document_portal_path (const char *path, + char **guessed_docid) +{ + const char *prefix = "/run/user/"; + char *docid; + char *p, *q; + + if (!g_str_has_prefix (path, prefix)) + return FALSE; + + p = strstr (path, "/doc/"); + if (!p) + return FALSE; + + p += strlen ("/doc/"); + q = strchr (p, '/'); + if (q) + docid = g_strndup (p, q - p); + else + docid = g_strdup (p); + + if (docid[0] == '\0') + { + g_free (docid); + return FALSE; + } + + *guessed_docid = docid; + return TRUE; +} + +static char * +get_host_folder_for_doc_id (const char *doc_id) +{ + g_autofree char *real_path = get_real_path_for_doc_id (doc_id); + g_autofree char *host_folder = NULL; + + if (real_path != NULL) + host_folder = g_path_get_dirname (real_path); + + return g_steal_pointer (&host_folder); +} + +static void +open_file_done (GObject *source, + GAsyncResult *result, + gpointer data) +{ + g_autoptr(Request) request = data; + guint response = 2; + g_autoptr(GVariant) options = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GTask) task = NULL; + + if (!xdp_dbus_impl_file_chooser_call_open_file_finish (XDP_DBUS_IMPL_FILE_CHOOSER (source), + &response, + &options, + result, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("Backend call failed: %s", error->message); + } + + g_object_set_data (G_OBJECT (request), "response", GINT_TO_POINTER (response)); + if (options) + g_object_set_data_full (G_OBJECT (request), "options", g_variant_ref (options), (GDestroyNotify)g_variant_unref); + + task = g_task_new (NULL, NULL, NULL, NULL); + g_task_set_task_data (task, g_object_ref (request), g_object_unref); + g_task_run_in_thread (task, send_response_in_thread_func); +} + +static gboolean +check_value_type (const char *key, + GVariant *value, + const GVariantType *type, + GError **error) +{ + if (g_variant_is_of_type (value, type)) + return TRUE; + + g_set_error (error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "expected type for key %s is %s, found %s", + key, (const char *)type, (const char *)g_variant_get_type (value)); + + return FALSE; +} + +static gboolean +check_filter (GVariant *filter, + GError **error) +{ + const char *name; + g_autoptr(GVariant) list = NULL; + int i; + + g_variant_get (filter, "(&s@a(us))", &name, &list); + + if (name[0] == 0) + { + g_set_error_literal (error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "name is empty"); + return FALSE; + } + + if (g_variant_n_children (list) == 0) + { + g_set_error_literal (error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "no filters"); + return FALSE; + } + + for (i = 0; i < g_variant_n_children (list); i++) + { + guint32 type; + const char *string; + + g_variant_get_child (list, i, "(u&s)", &type, &string); + if (type == 0) + { + /* TODO: validate glob */ + if (string[0] == 0) + { + g_set_error_literal (error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "invalid glob pattern"); + return FALSE; + } + } + else if (type == 1) + { + /* TODO: validate content type */ + if (string[0] == 0) + { + g_set_error_literal (error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "invalid content type"); + return FALSE; + } + } + else + { + g_set_error (error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "invalid filter type: %u", type); + return FALSE; + } + } + + return TRUE; +} + +static gboolean +validate_filters (const char *key, + GVariant *value, + GVariant *options, + GError **error) +{ + gsize i; + + if (!check_value_type ("filters", value, G_VARIANT_TYPE ("a(sa(us))"), error)) + return FALSE; + + for (i = 0; i < g_variant_n_children (value); i++) + { + g_autoptr(GVariant) filter = g_variant_get_child_value (value, i); + + if (!check_filter (filter, error)) + { + g_prefix_error (error, "invalid filter: "); + return FALSE; + } + } + + return TRUE; +} + +static gboolean +validate_current_filter (const char *key, + GVariant *value, + GVariant *options, + GError **error) +{ + g_autoptr(GVariant) filters = NULL; + gsize i, n_children; + + if (!check_value_type ("current_filter", value, G_VARIANT_TYPE ("(sa(us))"), error)) + return FALSE; + + if (!check_filter (value, error)) + { + g_prefix_error (error, "invalid filter: "); + return FALSE; + } + + /* If the filters list is nonempty and current_filter is specified, + * then the list must contain current_filter. But if the list is + * empty, current_filter may be anything. + */ + filters = g_variant_lookup_value (options, "filters", G_VARIANT_TYPE ("a(sa(us))")); + if (!filters) + return TRUE; + + if (!check_value_type ("filters", filters, G_VARIANT_TYPE ("a(sa(us))"), error)) + { + g_prefix_error (error, "filters list is invalid: "); + return FALSE; + } + + n_children = g_variant_n_children (filters); + if (n_children == 0) + return TRUE; + + for (i = 0; i < n_children; i++) + { + g_autoptr(GVariant) filter = g_variant_get_child_value (filters, i); + if (g_variant_equal (filter, value)) + return TRUE; + } + + g_set_error_literal (error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "current_filter, if specified, must be present in filters list if list is nonempty"); + return FALSE; +} + +static gboolean +check_choice (GVariant *choice, + GError **error) +{ + const char *id; + const char *label; + g_autoptr(GVariant) options = NULL; + const char *option; + int i; + gboolean seen_option; + + g_variant_get (choice, "(&s&s@a(ss)&s)", &id, &label, &options, &option); + + if (id[0] == 0) + { + g_set_error_literal (error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "id is empty"); + return FALSE; + } + + if (label[0] == 0) + { + g_set_error_literal (error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "label is empty"); + return FALSE; + } + + if (g_variant_n_children (options) == 0) + { + const char *values[] = { "", "true", "false", NULL }; + if (!g_strv_contains (values, option)) + { + g_set_error (error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "bad current option: %s", option); + return FALSE; + } + + return TRUE; + } + + seen_option = FALSE; + for (i = 0; i < g_variant_n_children (options); i++) + { + const char *o_id; + const char *o_label; + + g_variant_get_child (options, i, "(&s&s)", &o_id, &o_label); + + if (o_id[0] == 0) + { + g_set_error_literal (error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "option id is empty"); + return FALSE; + } + if (o_label[0] == 0) + { + g_set_error_literal (error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "option label is empty"); + return FALSE; + } + + if (strcmp (o_id, option) == 0) + seen_option = TRUE; + } + + if (!seen_option && option[0] != 0) + { + g_set_error (error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "bad current option: %s", option); + return FALSE; + } + + return TRUE; +} + +static gboolean +validate_choices (const char *key, + GVariant *value, + GVariant *options, + GError **error) +{ + int i; + + if (!check_value_type ("choices", value, G_VARIANT_TYPE ("a(ssa(ss)s)"), error)) + return FALSE; + + for (i = 0; i < g_variant_n_children (value); i++) + { + g_autoptr(GVariant) choice = g_variant_get_child_value (value, i); + + if (!check_choice (choice, error)) + { + g_prefix_error (error, "invalid choice: "); + return FALSE; + } + } + + return TRUE; +} + +/* Note: current_folder is intentionally left out */ +static XdpOptionKey open_file_options[] = { + { "accept_label", G_VARIANT_TYPE_STRING, NULL }, + { "modal", G_VARIANT_TYPE_BOOLEAN, NULL }, + { "multiple", G_VARIANT_TYPE_BOOLEAN, NULL }, + { "directory", G_VARIANT_TYPE_BOOLEAN, NULL }, + { "filters", (const GVariantType *)"a(sa(us))", validate_filters }, + { "current_filter", (const GVariantType *)"(sa(us))", validate_current_filter }, + { "choices", (const GVariantType *)"a(ssa(ss)s)", validate_choices }, +}; + +static gboolean +handle_open_file (XdpDbusFileChooser *object, + GDBusMethodInvocation *invocation, + const gchar *arg_parent_window, + const gchar *arg_title, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + const char *app_id = xdp_app_info_get_id (request->app_info); + g_autoptr(GError) error = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + GVariantBuilder options; + g_autoptr(GVariant) dir_option = NULL; + + g_debug ("Handling OpenFile"); + + REQUEST_AUTOLOCK (request); + + g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &options, + open_file_options, G_N_ELEMENTS (open_file_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + { + g_autoptr(GVariant) value = + g_variant_lookup_value (arg_options, "current_folder", G_VARIANT_TYPE_BYTESTRING); + + if (value) + { + const char *path_from_app = g_variant_get_bytestring (value); + g_autofree char *host_path = g_strdup (path_from_app); + g_autofree char *doc_id_from_app = NULL; + if (looks_like_document_portal_path (host_path, &doc_id_from_app)) + { + char *real_path = get_host_folder_for_doc_id (doc_id_from_app); + if (real_path) + { + g_free (host_path); + host_path = real_path; + } + g_debug ("OpenFile: translating current_folder value '%s' to host path '%s'", path_from_app, host_path); + } + g_variant_builder_add (&options, "{sv}", "current_folder", + g_variant_new_bytestring (host_path)); + } + } + + impl_request = + xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, &error); + if (!impl_request) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + dir_option = g_variant_lookup_value (arg_options, + "directory", + G_VARIANT_TYPE_BOOLEAN); + if (dir_option && g_variant_get_boolean (dir_option)) + g_object_set_data (G_OBJECT (request), "directory", GINT_TO_POINTER (TRUE)); + + request_set_impl_request (request, impl_request); + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + xdp_dbus_impl_file_chooser_call_open_file (impl, + request->id, + app_id, + arg_parent_window, + arg_title, + g_variant_builder_end (&options), + NULL, + open_file_done, + g_object_ref (request)); + + xdp_dbus_file_chooser_complete_open_file (object, invocation, request->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +/* Note that current_file and current_folder are intentionally left out here. + * It is handled separately below + */ +static XdpOptionKey save_file_options[] = { + { "accept_label", G_VARIANT_TYPE_STRING, NULL }, + { "modal", G_VARIANT_TYPE_BOOLEAN, NULL }, + { "filters", (const GVariantType *)"a(sa(us))", validate_filters }, + { "current_filter", (const GVariantType *)"(sa(us))", validate_current_filter }, + { "current_name", G_VARIANT_TYPE_STRING, NULL }, + { "choices", (const GVariantType *)"a(ssa(ss)s)", validate_choices } +}; + +static void +save_file_done (GObject *source, + GAsyncResult *result, + gpointer data) +{ + g_autoptr(Request) request = data; + guint response = 2; + g_autoptr(GVariant) options = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GTask) task = NULL; + + if (!xdp_dbus_impl_file_chooser_call_save_file_finish (XDP_DBUS_IMPL_FILE_CHOOSER (source), + &response, + &options, + result, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("Backend call failed: %s", error->message); + } + + g_object_set_data (G_OBJECT (request), "response", GINT_TO_POINTER (response)); + if (options) + g_object_set_data_full (G_OBJECT (request), "options", g_variant_ref (options), (GDestroyNotify)g_variant_unref); + + task = g_task_new (NULL, NULL, NULL, NULL); + g_task_set_task_data (task, g_object_ref (request), g_object_unref); + g_task_run_in_thread (task, send_response_in_thread_func); +} + +static gboolean +handle_save_file (XdpDbusFileChooser *object, + GDBusMethodInvocation *invocation, + const gchar *arg_parent_window, + const gchar *arg_title, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + const char *app_id = xdp_app_info_get_id (request->app_info); + g_autoptr(GError) error = NULL; + XdpDbusImplRequest *impl_request; + GVariantBuilder options; + + g_debug ("Handling SaveFile"); + + if (xdp_dbus_impl_lockdown_get_disable_save_to_disk (lockdown)) + { + g_debug ("File saving disabled"); + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "File saving disabled"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + REQUEST_AUTOLOCK (request); + + g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &options, + save_file_options, G_N_ELEMENTS (save_file_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + { + g_autoptr(GVariant) value = g_variant_lookup_value (arg_options, + "current_file", + G_VARIANT_TYPE_BYTESTRING); + + if (value) + { + const char *path = g_variant_get_bytestring (value); + g_autofree char *host_path = get_real_path_for_doc_path (path, request->app_info); + g_autofree char *doc_id = NULL; + + if (strcmp (path, host_path) == 0 && + looks_like_document_portal_path (path, &doc_id)) + { + char *real_path = get_real_path_for_doc_id (doc_id); + + if (real_path) + { + g_free (host_path); + host_path = real_path; + } + } + + g_debug ("SaveFile: translating current_file value '%s' to host path '%s'", path, host_path); + + g_variant_builder_add (&options, "{sv}", "current_file", g_variant_new_bytestring (host_path)); + } + } + { + g_autoptr(GVariant) value = + g_variant_lookup_value (arg_options, "current_folder", G_VARIANT_TYPE_BYTESTRING); + + if (value) + { + const char *path_from_app = g_variant_get_bytestring (value); + g_autofree char *host_path = g_strdup (path_from_app); + g_autofree char *doc_id_from_app = NULL; + if (looks_like_document_portal_path (host_path, &doc_id_from_app)) + { + char *real_path = get_host_folder_for_doc_id (doc_id_from_app); + if (real_path) + { + g_free (host_path); + host_path = real_path; + } + g_debug ("SaveFile: translating current_folder value '%s' to host path '%s'", path_from_app, host_path); + } + g_variant_builder_add (&options, "{sv}", "current_folder", + g_variant_new_bytestring (host_path)); + } + } + + impl_request = + xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, &error); + if (!impl_request) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_object_set_data (G_OBJECT (request), "for-save", GINT_TO_POINTER (TRUE)); + + request_set_impl_request (request, impl_request); + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + xdp_dbus_impl_file_chooser_call_save_file (impl, + request->id, + app_id, + arg_parent_window, + arg_title, + g_variant_builder_end (&options), + NULL, + save_file_done, + g_object_ref (request)); + + xdp_dbus_file_chooser_complete_save_file (object, invocation, request->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static XdpOptionKey save_files_options[] = { + { "accept_label", G_VARIANT_TYPE_STRING, NULL }, + { "modal", G_VARIANT_TYPE_BOOLEAN, NULL }, + { "current_name", G_VARIANT_TYPE_STRING, NULL }, + { "current_folder", G_VARIANT_TYPE_BYTESTRING, NULL }, + { "files", G_VARIANT_TYPE_BYTESTRING_ARRAY, NULL }, + { "choices", (const GVariantType *)"a(ssa(ss)s)", validate_choices } +}; + +static void +save_files_done (GObject *source, + GAsyncResult *result, + gpointer data) +{ + g_autoptr(Request) request = data; + guint response = 2; + g_autoptr(GVariant) options = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GTask) task = NULL; + + if (!xdp_dbus_impl_file_chooser_call_save_files_finish (XDP_DBUS_IMPL_FILE_CHOOSER (source), + &response, + &options, + result, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("Backend call failed: %s", error->message); + } + + g_object_set_data (G_OBJECT (request), "response", GINT_TO_POINTER (response)); + if (options) + g_object_set_data_full (G_OBJECT (request), "options", g_variant_ref (options), (GDestroyNotify)g_variant_unref); + + task = g_task_new (NULL, NULL, NULL, NULL); + g_task_set_task_data (task, g_object_ref (request), g_object_unref); + g_task_run_in_thread (task, send_response_in_thread_func); +} + +static gboolean +handle_save_files (XdpDbusFileChooser *object, + GDBusMethodInvocation *invocation, + const gchar *arg_parent_window, + const gchar *arg_title, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + const char *app_id = xdp_app_info_get_id (request->app_info); + g_autoptr(GError) error = NULL; + XdpDbusImplRequest *impl_request; + GVariantBuilder options; + + if (xdp_dbus_impl_lockdown_get_disable_save_to_disk (lockdown)) + { + g_debug ("File saving disabled"); + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "File saving disabled"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + REQUEST_AUTOLOCK (request); + + g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &options, + save_files_options, G_N_ELEMENTS (save_files_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + impl_request = + xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, &error); + if (!impl_request) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_object_set_data (G_OBJECT (request), "for-save", GINT_TO_POINTER (TRUE)); + + request_set_impl_request (request, impl_request); + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + xdp_dbus_impl_file_chooser_call_save_files (impl, + request->id, + app_id, + arg_parent_window, + arg_title, + g_variant_builder_end (&options), + NULL, + save_files_done, + g_object_ref (request)); + + xdp_dbus_file_chooser_complete_save_files (object, invocation, request->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +file_chooser_iface_init (XdpDbusFileChooserIface *iface) +{ + iface->handle_open_file = handle_open_file; + iface->handle_save_file = handle_save_file; + iface->handle_save_files = handle_save_files; +} + +static void +file_chooser_init (FileChooser *fc) +{ + xdp_dbus_file_chooser_set_version (XDP_DBUS_FILE_CHOOSER (fc), 3); +} + +static void +file_chooser_class_init (FileChooserClass *klass) +{ +} + +GDBusInterfaceSkeleton * +file_chooser_create (GDBusConnection *connection, + const char *dbus_name, + gpointer lockdown_proxy) +{ + g_autoptr(GError) error = NULL; + + lockdown = lockdown_proxy; + + impl = xdp_dbus_impl_file_chooser_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_NONE, + dbus_name, + DESKTOP_PORTAL_OBJECT_PATH, + NULL, + &error); + + if (impl == NULL) + { + g_warning ("Failed to create file chooser proxy: %s", error->message); + return NULL; + } + + g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (impl), G_MAXINT); + + file_chooser = g_object_new (file_chooser_get_type (), NULL); + + return G_DBUS_INTERFACE_SKELETON (file_chooser); +} diff --git a/src/file-chooser.h b/src/file-chooser.h new file mode 100644 index 0000000..be86c61 --- /dev/null +++ b/src/file-chooser.h @@ -0,0 +1,27 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +#pragma once + +#include + +GDBusInterfaceSkeleton * file_chooser_create (GDBusConnection *connection, + const char *dbus_name, + gpointer lockdown); diff --git a/src/flatpak-instance.c b/src/flatpak-instance.c new file mode 100644 index 0000000..23b5bd7 --- /dev/null +++ b/src/flatpak-instance.c @@ -0,0 +1,523 @@ +/* + * Copyright © 2018 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +#include "config.h" + +#include +#include + +#include "flatpak-instance.h" + +/** + * SECTION:flatpak-instance + * @Title: FlatpakInstance + * @Short_description: Information about a running sandbox + * + * A FlatpakInstance refers to a running sandbox, and contains + * some basic information about the sandbox setup, such as the + * application and runtime used inside the sandbox. + * + * Importantly, it also gives access to the PID of the main + * processes in the sandbox. + * + * One way to obtain FlatpakInstances is to use flatpak_instance_get_all(). + * Another way is to use flatpak_installation_launch_full(). + * + * Note that process lifecycle tracking is fundamentally racy. + * You have to be prepared for the sandbox and the processes + * represented by a FlatpakInstance to not be around anymore. + * + * The FlatpakInstance api was added in Flatpak 1.1. + */ + + +#define FLATPAK_METADATA_GROUP_APPLICATION "Application" +#define FLATPAK_METADATA_GROUP_RUNTIME "Runtime" +#define FLATPAK_METADATA_KEY_COMMAND "command" +#define FLATPAK_METADATA_KEY_NAME "name" +#define FLATPAK_METADATA_KEY_REQUIRED_FLATPAK "required-flatpak" +#define FLATPAK_METADATA_KEY_RUNTIME "runtime" +#define FLATPAK_METADATA_KEY_SDK "sdk" +#define FLATPAK_METADATA_KEY_TAGS "tags" + +#define FLATPAK_METADATA_GROUP_INSTANCE "Instance" +#define FLATPAK_METADATA_KEY_INSTANCE_PATH "instance-path" +#define FLATPAK_METADATA_KEY_INSTANCE_ID "instance-id" +#define FLATPAK_METADATA_KEY_APP_PATH "app-path" +#define FLATPAK_METADATA_KEY_APP_COMMIT "app-commit" +#define FLATPAK_METADATA_KEY_APP_EXTENSIONS "app-extensions" +#define FLATPAK_METADATA_KEY_ARCH "arch" +#define FLATPAK_METADATA_KEY_BRANCH "branch" +#define FLATPAK_METADATA_KEY_FLATPAK_VERSION "flatpak-version" +#define FLATPAK_METADATA_KEY_RUNTIME_PATH "runtime-path" +#define FLATPAK_METADATA_KEY_RUNTIME_COMMIT "runtime-commit" +#define FLATPAK_METADATA_KEY_RUNTIME_EXTENSIONS "runtime-extensions" +#define FLATPAK_METADATA_KEY_SESSION_BUS_PROXY "session-bus-proxy" +#define FLATPAK_METADATA_KEY_SYSTEM_BUS_PROXY "system-bus-proxy" +#define FLATPAK_METADATA_KEY_EXTRA_ARGS "extra-args" +#define FLATPAK_METADATA_KEY_SANDBOX "sandbox" +#define FLATPAK_METADATA_KEY_BUILD "build" + +typedef struct _FlatpakInstancePrivate FlatpakInstancePrivate; + +struct _FlatpakInstancePrivate +{ + char *id; + char *dir; + + GKeyFile *info; + char *app; + char *arch; + char *branch; + char *commit; + char *runtime; + char *runtime_commit; + + int pid; + int child_pid; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (FlatpakInstance, flatpak_instance, G_TYPE_OBJECT) + +static void +flatpak_instance_finalize (GObject *object) +{ + FlatpakInstance *self = FLATPAK_INSTANCE (object); + FlatpakInstancePrivate *priv = flatpak_instance_get_instance_private (self); + + g_free (priv->id); + g_free (priv->dir); + g_free (priv->app); + g_free (priv->arch); + g_free (priv->branch); + g_free (priv->commit); + g_free (priv->runtime); + g_free (priv->runtime_commit); + + if (priv->info) + g_key_file_unref (priv->info); + + G_OBJECT_CLASS (flatpak_instance_parent_class)->finalize (object); +} + +static void +flatpak_instance_class_init (FlatpakInstanceClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = flatpak_instance_finalize; +} + +static void +flatpak_instance_init (FlatpakInstance *self) +{ +} + +/** + * flatpak_instance_get_id: + * @self: a #FlatpakInstance + * + * Gets the instance ID. The ID is used by Flatpak for bookkeeping + * purposes and has no further relevance. + * + * Returns: the instance ID + * + * Since: 1.1 + */ +const char * +flatpak_instance_get_id (FlatpakInstance *self) +{ + FlatpakInstancePrivate *priv = flatpak_instance_get_instance_private (self); + + return priv->id; +} + +/** + * flatpak_instance_get_app: + * @self: a #FlatpakInstance + * + * Gets the application ID of the application running in the instance. + * + * Note that this may return %NULL for sandboxes that don't have an application. + * + * Returns: the application ID + * + * Since: 1.1 + */ +const char * +flatpak_instance_get_app (FlatpakInstance *self) +{ + FlatpakInstancePrivate *priv = flatpak_instance_get_instance_private (self); + + return priv->app; +} + +/** + * flatpak_instance_get_arch: + * @self: a #FlatpakInstance + * + * Gets the architecture of the application running in the instance. + * + * Returns: the architecture + * + * Since: 1.1 + */ +const char * +flatpak_instance_get_arch (FlatpakInstance *self) +{ + FlatpakInstancePrivate *priv = flatpak_instance_get_instance_private (self); + + return priv->arch; +} + +/** + * flatpak_instance_get_branch: + * @self: a #FlatpakInstance + * + * Gets the branch of the application running in the instance. + * + * Returns: the architecture + * + * Since: 1.1 + */ +const char * +flatpak_instance_get_branch (FlatpakInstance *self) +{ + FlatpakInstancePrivate *priv = flatpak_instance_get_instance_private (self); + + return priv->branch; +} + +/** + * flatpak_instance_get_commit: + * @self: a #FlatpakInstance + * + * Gets the commit of the application running in the instance. + * + * Returns: the commit + * + * Since: 1.1 + */ +const char * +flatpak_instance_get_commit (FlatpakInstance *self) +{ + FlatpakInstancePrivate *priv = flatpak_instance_get_instance_private (self); + + return priv->commit; +} + +/** + * flatpak_instance_get_runtime: + * @self: a #FlatpakInstance + * + * Gets the ref of the runtime used in the instance. + * + * Returns: the runtime ref + * + * Since: 1.1 + */ +const char * +flatpak_instance_get_runtime (FlatpakInstance *self) +{ + FlatpakInstancePrivate *priv = flatpak_instance_get_instance_private (self); + + return priv->runtime; +} + +/** + * flatpak_instance_get_runtime_commit: + * @self: a #FlatpakInstance + * + * Gets the commit of the runtime used in the instance. + * + * Returns: the runtime commit + * + * Since: 1.1 + */ +const char * +flatpak_instance_get_runtime_commit (FlatpakInstance *self) +{ + FlatpakInstancePrivate *priv = flatpak_instance_get_instance_private (self); + + return priv->runtime_commit; +} + +/** + * flatpak_instance_get_pid: + * @self: a #FlatpakInstance + * + * Gets the PID of the outermost process in the sandbox. This is not the + * application process itself, but a bubblewrap 'babysitter' process. + * + * See flatpak_instance_get_child_pid(). + * + * Returns: the outermost process PID + * + * Since: 1.1 + */ +int +flatpak_instance_get_pid (FlatpakInstance *self) +{ + FlatpakInstancePrivate *priv = flatpak_instance_get_instance_private (self); + + return priv->pid; +} + +static int get_child_pid (const char *dir); + +/** + * flatpak_instance_get_child_pid: + * @self: a #FlatpakInstance + * + * Gets the PID of the application process in the sandbox. + * + * See flatpak_instance_get_pid(). + * + * Note that this function may return 0 immediately after launching + * a sandbox, for a short amount of time. + * + * Returns: the application process PID + * + * Since: 1.1 + */ +int +flatpak_instance_get_child_pid (FlatpakInstance *self) +{ + FlatpakInstancePrivate *priv = flatpak_instance_get_instance_private (self); + + if (priv->child_pid == 0) + priv->child_pid = get_child_pid (priv->dir); + + return priv->child_pid; +} + +/** + * flatpak_instance_get_info: + * @self: a #FlatpakInstance + * + * Gets a keyfile that holds information about the running sandbox. + * + * This file is available as /.flatpak-info inside the sandbox as well. + * + * The most important data in the keyfile is available with separate getters, + * but there may be more information in the keyfile. + * + * Returns: the flatpak-info keyfile + * + * Since: 1.1 + */ +GKeyFile * +flatpak_instance_get_info (FlatpakInstance *self) +{ + FlatpakInstancePrivate *priv = flatpak_instance_get_instance_private (self); + + return priv->info; +} + +static GKeyFile * +get_instance_info (const char *dir) +{ + g_autofree char *file = NULL; + g_autoptr(GKeyFile) key_file = NULL; + g_autoptr(GError) error = NULL; + + file = g_build_filename (dir, "info", NULL); + + key_file = g_key_file_new (); + if (!g_key_file_load_from_file (key_file, file, G_KEY_FILE_NONE, &error)) + { + g_debug ("Failed to load instance info file '%s': %s", file, error->message); + return NULL; + } + + return g_steal_pointer (&key_file); +} + +static int +get_child_pid (const char *dir) +{ + g_autofree char *file = NULL; + g_autofree char *contents = NULL; + gsize length; + g_autoptr(GError) error = NULL; + g_autoptr(JsonParser) parser = NULL; + JsonNode *node; + JsonObject *obj; + + file = g_build_filename (dir, "bwrapinfo.json", NULL); + + if (!g_file_get_contents (file, &contents, &length, &error)) + { + g_debug ("Failed to load bwrapinfo.json file '%s': %s", file, error->message); + return 0; + } + + parser = json_parser_new (); + if (!json_parser_load_from_data (parser, contents, length, &error)) + { + g_debug ("Failed to parse bwrapinfo.json file '%s': %s", file, error->message); + return 0; + } + + node = json_parser_get_root (parser); + if (!node) + { + g_debug ("Failed to parse bwrapinfo.json file '%s': %s", file, "empty"); + return 0; + } + + obj = json_node_get_object (node); + + return json_object_get_int_member (obj, "child-pid"); +} + +static int +get_pid (const char *dir) +{ + g_autofree char *file = NULL; + g_autofree char *contents = NULL; + g_autoptr(GError) error = NULL; + + file = g_build_filename (dir, "pid", NULL); + + if (!g_file_get_contents (file, &contents, NULL, &error)) + { + g_debug ("Failed to load pid file '%s': %s", file, error->message); + return 0; + } + + return (int) g_ascii_strtoll (contents, NULL, 10); +} + +static FlatpakInstance * +flatpak_instance_new (const char *dir) +{ + FlatpakInstance *self = g_object_new (flatpak_instance_get_type (), NULL); + FlatpakInstancePrivate *priv = flatpak_instance_get_instance_private (self); + + priv->dir = g_strdup (dir); + priv->id = g_path_get_basename (dir); + + priv->pid = get_pid (priv->dir); + priv->child_pid = get_child_pid (priv->dir); + priv->info = get_instance_info (priv->dir); + + if (priv->info) + { + if (g_key_file_has_group (priv->info, FLATPAK_METADATA_GROUP_APPLICATION)) + { + priv->app = g_key_file_get_string (priv->info, + FLATPAK_METADATA_GROUP_APPLICATION, FLATPAK_METADATA_KEY_NAME, NULL); + priv->runtime = g_key_file_get_string (priv->info, + FLATPAK_METADATA_GROUP_APPLICATION, FLATPAK_METADATA_KEY_RUNTIME, NULL); + } + else + { + priv->runtime = g_key_file_get_string (priv->info, + FLATPAK_METADATA_GROUP_RUNTIME, FLATPAK_METADATA_KEY_RUNTIME, NULL); + } + + priv->arch = g_key_file_get_string (priv->info, + FLATPAK_METADATA_GROUP_INSTANCE, FLATPAK_METADATA_KEY_ARCH, NULL); + priv->branch = g_key_file_get_string (priv->info, + FLATPAK_METADATA_GROUP_INSTANCE, FLATPAK_METADATA_KEY_BRANCH, NULL); + priv->commit = g_key_file_get_string (priv->info, + FLATPAK_METADATA_GROUP_INSTANCE, FLATPAK_METADATA_KEY_APP_COMMIT, NULL); + priv->runtime_commit = g_key_file_get_string (priv->info, + FLATPAK_METADATA_GROUP_INSTANCE, FLATPAK_METADATA_KEY_RUNTIME_COMMIT, NULL); + } + + return self; +} + +static FlatpakInstance * +flatpak_instance_new_for_id (const char *id) +{ + g_autofree char *dir = NULL; + + dir = g_build_filename (g_get_user_runtime_dir (), ".flatpak", id, NULL); + return flatpak_instance_new (dir); +} + +/** + * flatpak_instance_get_all: + * + * Gets FlatpakInstance objects for all running sandboxes in the current session. + * + * Returns: (transfer full) (element-type FlatpakInstance): a #GPtrArray of + * #FlatpakInstance objects + * + * Since: 1.1 + */ +GPtrArray * +flatpak_instance_get_all (void) +{ + g_autoptr(GPtrArray) instances = NULL; + g_autofree char *base_dir = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GFileEnumerator) iter = NULL; + + instances = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + base_dir = g_build_filename (g_get_user_runtime_dir (), ".flatpak", NULL); + file = g_file_new_for_path (base_dir); + iter = g_file_enumerate_children (file, + G_FILE_ATTRIBUTE_STANDARD_NAME "," + G_FILE_ATTRIBUTE_STANDARD_TYPE, + G_FILE_QUERY_INFO_NONE, + NULL, + NULL); + if (!iter) + return g_steal_pointer (&instances); + + while (TRUE) + { + GFileInfo *info; + + if (!g_file_enumerator_iterate (iter, &info, NULL, NULL, NULL)) + break; + + if (!info) + break; + + if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY) + g_ptr_array_add (instances, flatpak_instance_new_for_id (g_file_info_get_name (info))); + } + + return g_steal_pointer (&instances); +} + +/** + * flatpak_instance_is_running: + * @self: a #FlatpakInstance + * + * Finds out if the sandbox represented by @self is still running. + * + * Returns: %TRUE if the sandbox is still running + */ +gboolean +flatpak_instance_is_running (FlatpakInstance *self) +{ + FlatpakInstancePrivate *priv = flatpak_instance_get_instance_private (self); + + if (kill (priv->pid, 0) == 0) + return TRUE; + + return FALSE; +} diff --git a/src/flatpak-instance.h b/src/flatpak-instance.h new file mode 100644 index 0000000..bbe0ce7 --- /dev/null +++ b/src/flatpak-instance.h @@ -0,0 +1,64 @@ +/* + * Copyright © 2018 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +#ifndef __FLATPAK_INSTANCE_H__ +#define __FLATPAK_INSTANCE_H__ + +typedef struct _FlatpakInstance FlatpakInstance; + +#include + +#define FLATPAK_TYPE_INSTANCE flatpak_instance_get_type () +#define FLATPAK_INSTANCE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), FLATPAK_TYPE_INSTANCE, FlatpakInstance)) +#define FLATPAK_IS_INSTANCE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), FLATPAK_TYPE_INSTANCE)) + +GType flatpak_instance_get_type (void); + +struct _FlatpakInstance +{ + GObject parent; +}; + +typedef struct +{ + GObjectClass parent_class; +} FlatpakInstanceClass; + + +#ifdef G_DEFINE_AUTOPTR_CLEANUP_FUNC +G_DEFINE_AUTOPTR_CLEANUP_FUNC (FlatpakInstance, g_object_unref) +#endif + +GPtrArray * flatpak_instance_get_all (void); + +const char * flatpak_instance_get_id (FlatpakInstance *self); +const char * flatpak_instance_get_app (FlatpakInstance *self); +const char * flatpak_instance_get_arch (FlatpakInstance *self); +const char * flatpak_instance_get_branch (FlatpakInstance *self); +const char * flatpak_instance_get_commit (FlatpakInstance *self); +const char * flatpak_instance_get_runtime (FlatpakInstance *self); +const char * flatpak_instance_get_runtime_commit (FlatpakInstance *self); +int flatpak_instance_get_pid (FlatpakInstance *self); +int flatpak_instance_get_child_pid (FlatpakInstance *self); +GKeyFile * flatpak_instance_get_info (FlatpakInstance *self); + +gboolean flatpak_instance_is_running (FlatpakInstance *self); + +#endif /* __FLATPAK_INSTANCE_H__ */ diff --git a/src/gamemode.c b/src/gamemode.c new file mode 100644 index 0000000..3c761f8 --- /dev/null +++ b/src/gamemode.c @@ -0,0 +1,607 @@ +/* + * Copyright © 2019 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Christian J. Kellner + */ + +#include "config.h" + +#include "request.h" +#include "permissions.h" + +#include "xdp-dbus.h" +#include "xdp-impl-dbus.h" +#include "xdp-utils.h" + +#include + +#define _GNU_SOURCE 1 +#include +#include +#include +#include +#include +#include +#include +#include /* unlinkat, fork */ + +/* well known names*/ +#define GAMEMODE_DBUS_NAME "com.feralinteractive.GameMode" +#define GAMEMODE_DBUS_IFACE "com.feralinteractive.GameMode" +#define GAMEMODE_DBUS_PATH "/com/feralinteractive/GameMode" + +#define PERMISSION_TABLE "gamemode" +#define PERMISSION_ID "gamemode" + +/* */ +typedef struct _GameMode GameMode; +typedef struct _GameModeClass GameModeClass; + +static gboolean handle_query_status (XdpDbusGameMode *object, + GDBusMethodInvocation *invocation, + gint pid); + +static gboolean handle_register_game (XdpDbusGameMode *object, + GDBusMethodInvocation *invocation, + gint pid); + +static gboolean handle_unregister_game (XdpDbusGameMode *object, + GDBusMethodInvocation *invocation, + gint pid); + +static gboolean handle_query_status_by_pid (XdpDbusGameMode *object, + GDBusMethodInvocation *invocation, + gint target, + gint requester); + +static gboolean handle_register_game_by_pid (XdpDbusGameMode *object, + GDBusMethodInvocation *invocation, + gint target, + gint requester); + +static gboolean handle_unregister_game_by_pid (XdpDbusGameMode *object, + GDBusMethodInvocation *invocation, + gint target, + gint requester); + +static gboolean handle_query_status_by_pidfd (XdpDbusGameMode *object, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + GVariant *arg_target, + GVariant *arg_requester); + +static gboolean handle_register_game_by_pidfd (XdpDbusGameMode *object, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + GVariant *arg_target, + GVariant *arg_requester); + +static gboolean handle_unregister_game_by_pidfd (XdpDbusGameMode *object, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + GVariant *arg_target, + GVariant *arg_requester); + + + +/* globals */ +static GameMode *gamemode; + +/* gobject */ + +struct _GameMode +{ + XdpDbusGameModeSkeleton parent_instance; + + /* */ + GDBusProxy *client; +}; + +struct _GameModeClass +{ + XdpDbusGameModeSkeletonClass parent_class; +}; + +GType game_mode_get_type (void) G_GNUC_CONST; +static void game_mode_iface_init (XdpDbusGameModeIface *iface); + +G_DEFINE_TYPE_WITH_CODE (GameMode, game_mode, XDP_DBUS_TYPE_GAME_MODE_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_GAME_MODE, + game_mode_iface_init)); + +static void +game_mode_iface_init (XdpDbusGameModeIface *iface) +{ + iface->handle_query_status = handle_query_status; + iface->handle_register_game = handle_register_game; + iface->handle_unregister_game = handle_unregister_game; + + iface->handle_query_status_by_pid = handle_query_status_by_pid; + iface->handle_register_game_by_pid = handle_register_game_by_pid; + iface->handle_unregister_game_by_pid = handle_unregister_game_by_pid; + + iface->handle_query_status_by_pidfd = handle_query_status_by_pidfd; + iface->handle_register_game_by_pidfd = handle_register_game_by_pidfd; + iface->handle_unregister_game_by_pidfd = handle_unregister_game_by_pidfd; + + +} + +static void +game_mode_init (GameMode *gamemode) +{ + xdp_dbus_game_mode_set_version (XDP_DBUS_GAME_MODE (gamemode), 4); +} + +static void +game_mode_class_init (GameModeClass *klass) +{ +} + +/* internal helpers */ + +static gboolean +game_mode_is_allowed_for_app (const char *app_id, GError **error) +{ + g_autoptr(GVariant) perms = NULL; + g_autoptr(GVariant) data = NULL; + g_autoptr(GError) err = NULL; + const char **stored; + gboolean ok; + + ok = xdp_dbus_impl_permission_store_call_lookup_sync (get_permission_store (), + PERMISSION_TABLE, + PERMISSION_ID, + &perms, + &data, + NULL, + &err); + + if (!ok) + { + g_dbus_error_strip_remote_error (err); + g_debug ("No gamemode permissions found: %s", err->message); + g_clear_error (&err); + } + else if (perms != NULL && g_variant_lookup (perms, app_id, "^a&s", &stored)) + { + g_autofree char *as_str = NULL; + gboolean allowed; + + as_str = g_strjoinv (" ", (char **)stored); + g_debug ("GameMode permissions for %s: %s", app_id, as_str); + + allowed = !g_strv_contains (stored, "no"); + + if (!allowed) + g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "GameMode is not allowed for %s", app_id); + + return allowed; + } + + g_debug ("No gamemode permissions stored for %s: allowing", app_id); + + return TRUE; +} + +static gboolean +check_pids(const pid_t *pids, gint count, GError **error) +{ + + for (gint i = 0; i < count; i++) { + if (pids[i] == 0) { + g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "pid %d is invalid (0)", i); + return FALSE; + } + } + + return TRUE; +} + +/* generic dbus call handling */ + +typedef struct CallData_ { + GDBusMethodInvocation *inv; + XdpAppInfo *app_info; + + char *method; + + int ids[2]; + guint n_ids; + + GUnixFDList *fdlist; + +} CallData; + +static CallData * +call_data_new (GDBusMethodInvocation *inv, + XdpAppInfo *app_info, + const char *method) +{ + CallData *call; + + call = g_slice_new0 (CallData); + + call->inv = g_object_ref (inv); + call->app_info = xdp_app_info_ref (app_info); + call->method = g_strdup (method); + + return call; +} + +static void +call_data_free (gpointer data) +{ + CallData *call = data; + if (call == NULL) + return; + + g_object_unref (call->inv); + xdp_app_info_unref (call->app_info); + g_free (call->method); + g_clear_object(&call->fdlist); + + g_slice_free (CallData, call); +} + +static void +handle_call_thread (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) res = NULL; + GUnixFDList *fdlist = NULL; + GVariant *params; + const char *app_id; + CallData *call; + gboolean ok; + gint r; + + call = (CallData *) task_data; + app_id = xdp_app_info_get_id (call->app_info); + + if (!game_mode_is_allowed_for_app (app_id, &error)) + { + g_dbus_method_invocation_return_gerror (call->inv, error); + return; + } + + /* if we don't have a list of fds, we got pids and need to map them */ + if (call->fdlist == NULL) + { + pid_t pids[2] = {0, }; + guint n_pids; + + n_pids = call->n_ids; + + for (guint i = 0; i < n_pids; i++) + pids[0] = (pid_t) call->ids[i]; + + ok = xdp_app_info_map_pids (call->app_info, pids, n_pids, &error); + + if (!ok) + { + g_prefix_error (&error, "Could not map pids: "); + g_warning ("GameMode error: %s", error->message); + g_dbus_method_invocation_return_gerror (call->inv, error); + return; + } + + if (n_pids == 1) + params = g_variant_new ("(i)", (gint32) pids[0]); + else + params = g_variant_new ("(ii)", (gint32) pids[0], (gint32) pids[1]); + + } + else + { + pid_t pids[2] = {0, }; + const int *fds; + gint n_pids; + + fdlist = call->fdlist; + + /* verify fds are actually pidfds */ + fds = g_unix_fd_list_peek_fds (fdlist, &n_pids); + + ok = xdp_app_info_pidfds_to_pids (call->app_info, fds, pids, n_pids, &error); + + if (!ok || !check_pids (pids, n_pids, &error)) + { + g_warning ("Pidfd verification error: %s", error->message); + g_dbus_method_invocation_return_error (call->inv, + G_DBUS_ERROR, + G_DBUS_ERROR_INVALID_ARGS, + "failed to verify fds as pidfds: %s", + error->message); + return; + } + + params = g_variant_new ("(hh)", 0, 1); + } + + res = g_dbus_proxy_call_with_unix_fd_list_sync (G_DBUS_PROXY (gamemode->client), + call->method, + params, + G_DBUS_CALL_FLAGS_NONE, + -1, + fdlist, + NULL, + NULL, /* cancel */ + &error); + + r = -2; /* default to "call got rejected" */ + if (res != NULL) + g_variant_get (res, "(i)", &r); + else + g_debug ("Call to GameMode failed: %s", error->message); + + g_dbus_method_invocation_return_value (call->inv, g_variant_new ("(i)", r)); +} + +static void +handle_call_in_thread_fds (XdpDbusGameMode *object, + const char *method, + GDBusMethodInvocation *invocation, + GUnixFDList *fdlist) +{ + g_autoptr(GTask) task = NULL; + XdpAppInfo *app_info; + Request *request; + CallData *call; + + if (fdlist == NULL || g_unix_fd_list_get_length (fdlist) != 2) + { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, + "File descriptor number is incorrect"); + return; + } + + request = request_from_invocation (invocation); + app_info = request->app_info; + + call = call_data_new (invocation, app_info, method); + call->fdlist = g_object_ref (fdlist); + + task = g_task_new (object, NULL, NULL, NULL); + + g_task_set_task_data (task, call, call_data_free); + g_task_run_in_thread (task, handle_call_thread); +} + +static void +handle_call_in_thread (XdpDbusGameMode *object, + const char *method, + GDBusMethodInvocation *invocation, + gint target, + gint requester) +{ + g_autoptr(GTask) task = NULL; + XdpAppInfo *app_info; + Request *request; + CallData *call; + + request = request_from_invocation (invocation); + app_info = request->app_info; + + call = call_data_new (invocation, app_info, method); + + call->ids[0] = target; + call->n_ids = 1; + + if (requester != 0) + { + call->ids[1] = requester; + call->n_ids += 1; + } + + task = g_task_new (object, NULL, NULL, NULL); + + g_task_set_task_data (task, call, call_data_free); + g_task_run_in_thread (task, handle_call_thread); +} + +/* dbus */ +static gboolean +handle_query_status (XdpDbusGameMode *object, + GDBusMethodInvocation *invocation, + gint pid) +{ + handle_call_in_thread (object, "QueryStatus", invocation, pid, 0); + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_register_game (XdpDbusGameMode *object, + GDBusMethodInvocation *invocation, + gint pid) +{ + handle_call_in_thread (object, "RegisterGame", invocation, pid, 0); + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_unregister_game (XdpDbusGameMode *object, + GDBusMethodInvocation *invocation, + gint pid) +{ + handle_call_in_thread (object, "UnregisterGame", invocation, pid, 0); + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_query_status_by_pid (XdpDbusGameMode *object, + GDBusMethodInvocation *invocation, + gint target, + gint requester) +{ + handle_call_in_thread (object, + "QueryStatusByPID", + invocation, + target, + requester); + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_register_game_by_pid (XdpDbusGameMode *object, + GDBusMethodInvocation *invocation, + gint target, + gint requester) +{ + handle_call_in_thread (object, + "RegisterGameByPID", + invocation, + target, + requester); + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_unregister_game_by_pid (XdpDbusGameMode *object, + GDBusMethodInvocation *invocation, + gint target, + gint requester) +{ + handle_call_in_thread (object, + "UnregisterGameByPID", + invocation, + target, + requester); + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +/* pidfd based APIs */ +static gboolean +handle_query_status_by_pidfd (XdpDbusGameMode *object, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + GVariant *arg_target, + GVariant *arg_requester) +{ + handle_call_in_thread_fds (object, + "QueryStatusByPIDFd", + invocation, + fd_list); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_register_game_by_pidfd (XdpDbusGameMode *object, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + GVariant *arg_target, + GVariant *arg_requester) +{ + handle_call_in_thread_fds (object, + "RegisterGameByPIDFd", + invocation, + fd_list); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_unregister_game_by_pidfd (XdpDbusGameMode *object, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + GVariant *arg_target, + GVariant *arg_requester) +{ + handle_call_in_thread_fds (object, + "UnregisterGameByPIDFd", + invocation, + fd_list); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + + +/* properties */ +static void +update_active_state (GVariant *client_count) +{ + gboolean enabled = g_variant_get_int32 (client_count) > 0; + xdp_dbus_game_mode_set_active (XDP_DBUS_GAME_MODE (gamemode), enabled); +} + +static void +update_active_state_from_cache (GDBusProxy *proxy) +{ + g_autoptr(GVariant) client_count = NULL; + + client_count = g_dbus_proxy_get_cached_property (proxy, "ClientCount"); + + if (client_count != NULL) + update_active_state (client_count); +} + +static void +client_properties_changed (GDBusProxy *proxy, + GVariant *changed_properties, + char **invalidated_properties) +{ + g_autoptr(GVariant) value = NULL; + + value = g_variant_lookup_value (changed_properties, "ClientCount", + G_VARIANT_TYPE_INT32); + + if (value != NULL) + update_active_state (value); +} + + +/* public API */ +GDBusInterfaceSkeleton * +game_mode_create (GDBusConnection *connection) +{ + g_autoptr(GError) err = NULL; + GDBusProxy *client; + GDBusProxyFlags flags; + + flags = G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START_AT_CONSTRUCTION | + G_DBUS_PROXY_FLAGS_GET_INVALIDATED_PROPERTIES; + client = g_dbus_proxy_new_sync (connection, + flags, + NULL, + GAMEMODE_DBUS_NAME, + GAMEMODE_DBUS_PATH, + GAMEMODE_DBUS_IFACE, + NULL, + &err); + + if (client == NULL) + { + g_warning ("Failed to create GameMode proxy: %s", err->message); + return NULL; + } + + gamemode = g_object_new (game_mode_get_type (), NULL); + gamemode->client = client; + + g_signal_connect (client, "g-properties-changed", + G_CALLBACK (client_properties_changed), NULL); + + update_active_state_from_cache (client); + + return G_DBUS_INTERFACE_SKELETON (gamemode);; +} diff --git a/src/gamemode.h b/src/gamemode.h new file mode 100644 index 0000000..e53fa13 --- /dev/null +++ b/src/gamemode.h @@ -0,0 +1,25 @@ +/* + * Copyright © 2019 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Christian J. Kellner + */ + +#pragma once + +#include + +GDBusInterfaceSkeleton * game_mode_create (GDBusConnection *connection); diff --git a/src/glib-backports.c b/src/glib-backports.c new file mode 100644 index 0000000..c4d7207 --- /dev/null +++ b/src/glib-backports.c @@ -0,0 +1,72 @@ +/* + * Copyright © 2014 Red Hat, Inc + * Copyright © 2021 Joshua Lee + * Copyright © 2021 Emmanuel Fleury + * Copyright © 2021 Nelson Ben + * Copyright © 2021 Peter Bloomfield + * Copyright © 2021 Collabora Ltd. + * Copyright 2023 Igalia + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#include "config.h" + +#include + +#include "glib-backports.h" + +#if !GLIB_CHECK_VERSION (2, 68, 0) +/* All this code is backported directly from glib */ +guint +g_string_replace (GString *string, + const gchar *find, + const gchar *replace, + guint limit) +{ + gsize f_len, r_len, pos; + gchar *cur, *next; + guint n = 0; + + g_return_val_if_fail (string != NULL, 0); + g_return_val_if_fail (find != NULL, 0); + g_return_val_if_fail (replace != NULL, 0); + + f_len = strlen (find); + r_len = strlen (replace); + cur = string->str; + + while ((next = strstr (cur, find)) != NULL) + { + pos = next - string->str; + g_string_erase (string, pos, f_len); + g_string_insert (string, pos, replace); + cur = string->str + pos + r_len; + n++; + /* Only match the empty string once at any given position, to + * avoid infinite loops */ + if (f_len == 0) + { + if (cur[0] == '\0') + break; + else + cur++; + } + if (n == limit) + break; + } + + return n; +} +#endif /* GLIB_CHECK_VERSION (2, 68, 0) */ diff --git a/src/glib-backports.h b/src/glib-backports.h new file mode 100644 index 0000000..0ac5993 --- /dev/null +++ b/src/glib-backports.h @@ -0,0 +1,42 @@ +/* + * Copyright © 2014, 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Alexander Larsson + * Matthias Clasen + */ + +#pragma once + +#include + +#if !GLIB_CHECK_VERSION (2, 68, 0) +guint g_string_replace (GString *string, + const gchar *find, + const gchar *replace, + guint limit); +#endif + +#if !GLIB_CHECK_VERSION (2, 68, 0) +static inline void +g_log_writer_default_set_use_stderr (gboolean use_stderr) +{ + /* Does nothing because outside of the tests we don't really care that it + * doesn't work correctly after this call and those tests can run on newer + * GLibs + */ +} +#endif diff --git a/src/global-shortcuts.c b/src/global-shortcuts.c new file mode 100644 index 0000000..9faeafd --- /dev/null +++ b/src/global-shortcuts.c @@ -0,0 +1,683 @@ +/* + * Copyright © 2022 Aleix Pol Gonzalez + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Aleix Pol Gonzalez + */ + +#include "config.h" + +#include +#include + +#include "global-shortcuts.h" +#include "request.h" +#include "session.h" +#include "permissions.h" +#include "xdp-dbus.h" +#include "xdp-impl-dbus.h" +#include "xdp-utils.h" + +typedef struct _GlobalShortcuts GlobalShortcuts; +typedef struct _GlobalShortcutsClass GlobalShortcutsClass; + +static GQuark quark_request_session; + +struct _GlobalShortcuts +{ + XdpDbusGlobalShortcutsSkeleton parent_instance; +}; + +struct _GlobalShortcutsClass +{ + XdpDbusGlobalShortcutsSkeletonClass parent_class; +}; + +static XdpDbusImplGlobalShortcuts *impl; +static GlobalShortcuts *global_shortcuts; + +GType global_shortcuts_get_type (void) G_GNUC_CONST; +static void global_shortcuts_iface_init (XdpDbusGlobalShortcutsIface *iface); + +G_DEFINE_TYPE_WITH_CODE (GlobalShortcuts, global_shortcuts, XDP_DBUS_TYPE_GLOBAL_SHORTCUTS_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_GLOBAL_SHORTCUTS, global_shortcuts_iface_init)); + +typedef struct _GlobalShortcutsSession +{ + Session parent; + + gboolean closed; +} GlobalShortcutsSession; + +typedef struct _GlobalShortcutsSessionClass +{ + SessionClass parent_class; +} GlobalShortcutsSessionClass; + +GType global_shortcuts_session_get_type (void); + +G_DEFINE_TYPE (GlobalShortcutsSession, global_shortcuts_session, session_get_type ()) + +static void +global_shortcuts_session_close (Session *session) +{ + GlobalShortcutsSession *global_shortcuts_session = (GlobalShortcutsSession *)session; + + global_shortcuts_session->closed = TRUE; +} + +static void +global_shortcuts_session_finalize (GObject *object) +{ + G_OBJECT_CLASS (global_shortcuts_session_parent_class)->finalize (object); +} + +static void +global_shortcuts_session_init (GlobalShortcutsSession *global_shortcuts_session) +{ +} + +static void +global_shortcuts_session_class_init (GlobalShortcutsSessionClass *klass) +{ + GObjectClass *object_class; + SessionClass *session_class; + + object_class = G_OBJECT_CLASS (klass); + object_class->finalize = global_shortcuts_session_finalize; + + session_class = (SessionClass *)klass; + session_class->close = global_shortcuts_session_close; +} + +static GlobalShortcutsSession * +global_shortcuts_session_new (GVariant *options, + Request *request, + GError **error) +{ + Session *session; + GDBusInterfaceSkeleton *interface_skeleton = + G_DBUS_INTERFACE_SKELETON (request); + const char *session_token; + GDBusConnection *connection = + g_dbus_interface_skeleton_get_connection (interface_skeleton); + GDBusConnection *impl_connection = + g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)); + const char *impl_dbus_name = g_dbus_proxy_get_name (G_DBUS_PROXY (impl)); + + session_token = lookup_session_token (options); + session = g_initable_new (global_shortcuts_session_get_type (), NULL, error, + "sender", request->sender, + "app-id", xdp_app_info_get_id (request->app_info), + "token", session_token, + "connection", connection, + "impl-connection", impl_connection, + "impl-dbus-name", impl_dbus_name, + NULL); + + if (session) + g_debug ("global shortcuts session owned by '%s' created", session->sender); + + return (GlobalShortcutsSession *) session; +} + +static void +session_created_cb (GObject *source_object, + GAsyncResult *res, + gpointer data) +{ + g_autoptr(Request) request = data; + Session *session; + guint response = 2; + g_autoptr (GVariant) results = NULL; + gboolean should_close_session; + GVariantBuilder results_builder; + g_autoptr(GError) error = NULL; + + REQUEST_AUTOLOCK (request); + + session = g_object_get_qdata (G_OBJECT (request), quark_request_session); + SESSION_AUTOLOCK_UNREF (g_object_ref (session)); + g_object_set_qdata (G_OBJECT (request), quark_request_session, NULL); + + g_variant_builder_init (&results_builder, G_VARIANT_TYPE_VARDICT); + + if (!xdp_dbus_impl_global_shortcuts_call_create_session_finish (impl, + &response, + &results, + res, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("A backend call failed: %s", error->message); + should_close_session = TRUE; + goto out; + } + + if (request->exported && response == 0) + { + if (!session_export (session, &error)) + { + g_warning ("Failed to export session: %s", error->message); + response = 2; + should_close_session = TRUE; + goto out; + } + + should_close_session = FALSE; + session_register (session); + } + else + { + should_close_session = TRUE; + } + + g_variant_builder_add (&results_builder, "{sv}", + "session_handle", g_variant_new ("s", session->id)); + +out: + if (request->exported) + { + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + response, + g_variant_builder_end (&results_builder)); + request_unexport (request); + } + else + { + g_variant_builder_clear (&results_builder); + } + + if (should_close_session) + session_close (session, FALSE); +} + +static XdpOptionKey global_shortcuts_create_session_options[] = { + { "handle_token", G_VARIANT_TYPE_STRING, NULL }, + { "session_handle_token", G_VARIANT_TYPE_STRING, NULL }, +}; + +static gboolean +handle_create_session (XdpDbusGlobalShortcuts *object, + GDBusMethodInvocation *invocation, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + g_autoptr(GError) error = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + GVariantBuilder options_builder; + g_autoptr(GVariant) options = NULL; + Session *session; + + REQUEST_AUTOLOCK (request); + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &options_builder, + global_shortcuts_create_session_options, + G_N_ELEMENTS (global_shortcuts_create_session_options), + &error)) + { + g_variant_builder_clear(&options_builder); + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + options = g_variant_builder_end (&options_builder); + impl_request = + xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, &error); + if (!impl_request) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request (request, impl_request); + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + session = (Session *)global_shortcuts_session_new (options, request, &error); + if (!session) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_object_set_qdata_full (G_OBJECT (request), + quark_request_session, + g_object_ref (session), + g_object_unref); + + xdp_dbus_impl_global_shortcuts_call_create_session (impl, + request->id, + session->id, + xdp_app_info_get_id (request->app_info), + g_steal_pointer (&options), + NULL, + session_created_cb, + g_object_ref (request)); + + xdp_dbus_global_shortcuts_complete_create_session (object, invocation, request->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +void +shortcuts_bound_cb (GObject *source_object, + GAsyncResult *res, + gpointer data) +{ + g_autoptr(Request) request = data; + Session *session; + guint response = 2; + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) results = NULL; + + REQUEST_AUTOLOCK (request); + + session = g_object_get_qdata (G_OBJECT (request), quark_request_session); + SESSION_AUTOLOCK_UNREF (g_object_ref (session)); + g_object_set_qdata (G_OBJECT (request), quark_request_session, NULL); + + if (!xdp_dbus_impl_global_shortcuts_call_bind_shortcuts_finish (impl, &response, &results, res, &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("A backend call failed: %s", error->message); + } + + if (request->exported) + { + if (!results) + { + GVariantBuilder results_builder; + + g_variant_builder_init (&results_builder, G_VARIANT_TYPE_VARDICT); + results = g_variant_ref_sink (g_variant_builder_end (&results_builder)); + } + + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), response, results); + request_unexport (request); + } +} + +static XdpOptionKey global_shortcuts_keys[] = { + { "description", G_VARIANT_TYPE_STRING, NULL }, + { "preferred_trigger", G_VARIANT_TYPE_STRING, NULL }, +}; + +static gboolean +xdp_verify_shortcuts (GVariant *shortcuts, + GVariantBuilder *filtered, + GError **error) +{ + gchar *shortcut_name; + GVariant *values = NULL; + g_autoptr(GVariantIter) iter = NULL; + + iter = g_variant_iter_new (shortcuts); + while (g_variant_iter_loop (iter, "(s@a{sv})", &shortcut_name, &values)) + { + GVariantBuilder shortcut_builder; + + if (shortcut_name[0] == 0) + { + g_set_error (error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Unexpected empty shortcut id"); + return FALSE; + } + + g_variant_builder_init (&shortcut_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (values, &shortcut_builder, + global_shortcuts_keys, + G_N_ELEMENTS (global_shortcuts_keys), + error)) + return FALSE; + g_variant_builder_add (filtered, "(sa{sv})", + shortcut_name, + &shortcut_builder); + } + return TRUE; +} + +static XdpOptionKey global_shortcuts_bind_shortcuts_options[] = { + { "handle_token", G_VARIANT_TYPE_STRING, NULL }, +}; + +static gboolean +handle_bind_shortcuts (XdpDbusGlobalShortcuts *object, + GDBusMethodInvocation *invocation, + const gchar *arg_session_handle, + GVariant *arg_shortcuts, + const gchar *arg_parent_window, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + Session *session; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) options = NULL; + g_autoptr(GVariant) shortcuts = NULL; + GVariantBuilder shortcuts_builder; + GVariantBuilder options_builder; + + REQUEST_AUTOLOCK (request); + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &options_builder, + global_shortcuts_bind_shortcuts_options, + G_N_ELEMENTS (global_shortcuts_bind_shortcuts_options), + &error)) + { + g_variant_builder_clear (&options_builder); + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + options = g_variant_builder_end (&options_builder); + + g_variant_builder_init (&shortcuts_builder, G_VARIANT_TYPE_ARRAY); + if (!xdp_verify_shortcuts (arg_shortcuts, &shortcuts_builder, + &error)) + { + g_variant_builder_clear (&shortcuts_builder); + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + shortcuts = g_variant_builder_end (&shortcuts_builder); + + session = acquire_session (arg_session_handle, request); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + impl_request = + xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, &error); + if (!impl_request) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request (request, impl_request); + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + g_object_set_qdata_full (G_OBJECT (request), + quark_request_session, + g_object_ref (session), + g_object_unref); + + xdp_dbus_impl_global_shortcuts_call_bind_shortcuts (impl, + request->id, + arg_session_handle, + g_steal_pointer (&shortcuts), + arg_parent_window, + g_steal_pointer (&options), + NULL, + shortcuts_bound_cb, + g_object_ref (request)); + + xdp_dbus_global_shortcuts_complete_bind_shortcuts (object, invocation, request->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +shortcuts_listed_cb (GObject *source_object, + GAsyncResult *res, + gpointer data) +{ + g_autoptr(Request) request = data; + Session *session; + guint response = 2; + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) results = NULL; + + REQUEST_AUTOLOCK (request); + + session = g_object_get_qdata (G_OBJECT (request), quark_request_session); + SESSION_AUTOLOCK_UNREF (g_object_ref (session)); + g_object_set_qdata (G_OBJECT (request), quark_request_session, NULL); + + if (!xdp_dbus_impl_global_shortcuts_call_list_shortcuts_finish (impl, &response, &results, res, &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("A backend call failed: %s", error->message); + } + + if (request->exported) + { + if (!results) + { + GVariantBuilder results_builder; + + g_variant_builder_init (&results_builder, G_VARIANT_TYPE_VARDICT); + results = g_variant_ref_sink (g_variant_builder_end (&results_builder)); + } + + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), response, results); + request_unexport (request); + } +} + +static XdpOptionKey global_shortcuts_list_shortcuts_options[] = { + { "handle_token", G_VARIANT_TYPE_STRING, NULL }, +}; + +static gboolean +handle_list_shortcuts (XdpDbusGlobalShortcuts *object, + GDBusMethodInvocation *invocation, + const gchar *arg_session_handle, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + Session *session; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + g_autoptr(GError) error = NULL; + GVariantBuilder options_builder; + g_autoptr(GVariant) options = NULL; + + REQUEST_AUTOLOCK (request); + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &options_builder, + global_shortcuts_list_shortcuts_options, + G_N_ELEMENTS (global_shortcuts_list_shortcuts_options), + &error)) + { + g_variant_builder_clear (&options_builder); + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + options = g_variant_builder_end (&options_builder); + + session = acquire_session (arg_session_handle, request); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + impl_request = + xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, &error); + if (!impl_request) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request (request, impl_request); + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + g_object_set_qdata_full (G_OBJECT (request), + quark_request_session, + g_object_ref (session), + g_object_unref); + + xdp_dbus_impl_global_shortcuts_call_list_shortcuts (impl, + request->id, + arg_session_handle, + NULL, + shortcuts_listed_cb, + g_object_ref (request)); + + xdp_dbus_global_shortcuts_complete_list_shortcuts (object, invocation, request->id); + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +global_shortcuts_iface_init (XdpDbusGlobalShortcutsIface *iface) +{ + iface->handle_create_session = handle_create_session; + iface->handle_bind_shortcuts = handle_bind_shortcuts; + iface->handle_list_shortcuts = handle_list_shortcuts; +} + +static void +global_shortcuts_init (GlobalShortcuts *global_shortcuts) +{ + xdp_dbus_global_shortcuts_set_version (XDP_DBUS_GLOBAL_SHORTCUTS (global_shortcuts), 1); +} + +static void +global_shortcuts_class_init (GlobalShortcutsClass *klass) +{ + quark_request_session = + g_quark_from_static_string ("-xdp-request-global-shortcuts-session"); +} + +static void +activated_cb (XdpDbusImplGlobalShortcuts *impl, + const char *session_id, + const char *shortcut_id, + guint64 timestamp, + GVariant *options, + gpointer data) +{ + GDBusConnection *connection = g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)); + g_autoptr(Session) session = lookup_session (session_id); + GlobalShortcutsSession *global_shortcuts_session = (GlobalShortcutsSession *)session; + + g_debug ("Received activated %s for %s", session_id, shortcut_id); + + if (global_shortcuts_session && !global_shortcuts_session->closed) + g_dbus_connection_emit_signal (connection, + session->sender, + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.GlobalShortcuts", + "Activated", + g_variant_new ("(ost@a{sv})", + session_id, shortcut_id, + timestamp, options), + NULL); +} + +static void +deactivated_cb (XdpDbusImplGlobalShortcuts *impl, + const char *session_id, + const char *shortcut_id, + guint64 timestamp, + GVariant *options, + gpointer data) +{ + GDBusConnection *connection = g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)); + g_autoptr(Session) session = lookup_session (session_id); + GlobalShortcutsSession *global_shortcuts_session = (GlobalShortcutsSession *)session; + + g_debug ("Received deactivated %s for %s", session_id, shortcut_id); + + if (global_shortcuts_session && !global_shortcuts_session->closed) + g_dbus_connection_emit_signal (connection, + session->sender, + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.GlobalShortcuts", + "Deactivated", + g_variant_new ("(ost@a{sv})", + session_id, shortcut_id, + timestamp, options), + NULL); +} + +static void +shortcuts_changed_cb (XdpDbusImplGlobalShortcuts *impl, + const char *session_id, + GVariant *shortcuts, + gpointer data) +{ + GDBusConnection *connection = g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)); + g_autoptr(Session) session = lookup_session (session_id); + GlobalShortcutsSession *global_shortcuts_session = (GlobalShortcutsSession *)session; + + g_debug ("Received ShortcutsChanged %s", session_id); + + if (global_shortcuts_session && !global_shortcuts_session->closed) + g_dbus_connection_emit_signal (connection, + session->sender, + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.GlobalShortcuts", + "ShortcutsChanged", + g_variant_new ("(o@a(sa{sv}))", session_id, shortcuts), + NULL); +} + +GDBusInterfaceSkeleton * +global_shortcuts_create (GDBusConnection *connection, + const char *dbus_name) +{ + g_autoptr(GError) error = NULL; + + impl = xdp_dbus_impl_global_shortcuts_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_NONE, + dbus_name, + "/org/freedesktop/portal/desktop", + NULL, &error); + if (impl == NULL) + { + g_warning ("Failed to create global_shortcuts proxy: %s", error->message); + return NULL; + } + + g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (impl), G_MAXINT); + global_shortcuts = g_object_new (global_shortcuts_get_type (), NULL); + + g_signal_connect (impl, "activated", G_CALLBACK (activated_cb), global_shortcuts); + g_signal_connect (impl, "deactivated", G_CALLBACK (deactivated_cb), global_shortcuts); + g_signal_connect (impl, "shortcuts-changed", G_CALLBACK (shortcuts_changed_cb), global_shortcuts); + + return G_DBUS_INTERFACE_SKELETON (global_shortcuts); +} diff --git a/src/global-shortcuts.h b/src/global-shortcuts.h new file mode 100644 index 0000000..beab1a2 --- /dev/null +++ b/src/global-shortcuts.h @@ -0,0 +1,27 @@ +/* + * Copyright © 2022 Aleix Pol Gonzalez + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Aleix Pol Gonzalez + */ + +#pragma once + +#include + +GDBusInterfaceSkeleton * +global_shortcuts_create (GDBusConnection *connection, + const char *dbus_name); diff --git a/src/inhibit.c b/src/inhibit.c new file mode 100644 index 0000000..88afd0c --- /dev/null +++ b/src/inhibit.c @@ -0,0 +1,539 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +#include "config.h" + +#include +#include + +#include "inhibit.h" +#include "request.h" +#include "session.h" +#include "permissions.h" +#include "xdp-dbus.h" +#include "xdp-impl-dbus.h" +#include "xdp-utils.h" + +#define PERMISSION_TABLE "inhibit" +#define PERMISSION_ID "inhibit" + +enum { + INHIBIT_LOGOUT = 1, + INHIBIT_USER_SWITCH = 2, + INHIBIT_SUSPEND = 4, + INHIBIT_IDLE = 8 +}; + +#define INHIBIT_ALL (INHIBIT_LOGOUT|INHIBIT_USER_SWITCH|INHIBIT_SUSPEND|INHIBIT_IDLE) + +typedef struct _Inhibit Inhibit; +typedef struct _InhibitClass InhibitClass; + +struct _Inhibit +{ + XdpDbusInhibitSkeleton parent_instance; +}; + +struct _InhibitClass +{ + XdpDbusInhibitSkeletonClass parent_class; +}; + +static XdpDbusImplInhibit *impl; +static Inhibit *inhibit; + +GType inhibit_get_type (void) G_GNUC_CONST; +static void inhibit_iface_init (XdpDbusInhibitIface *iface); + +G_DEFINE_TYPE_WITH_CODE (Inhibit, inhibit, XDP_DBUS_TYPE_INHIBIT_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_INHIBIT, + inhibit_iface_init)); + +static void +inhibit_done (GObject *source, + GAsyncResult *result, + gpointer data) +{ + g_autoptr(GError) error = NULL; + Request *request = data; + int response = 0; + + REQUEST_AUTOLOCK (request); + + if (!xdp_dbus_impl_inhibit_call_inhibit_finish (impl, result, &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("A backend call failed: %s", error->message); + response = 2; + } + + if (request->exported) + { + GVariantBuilder new_results; + + g_variant_builder_init (&new_results, G_VARIANT_TYPE_VARDICT); + + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + response, + g_variant_builder_end (&new_results)); + } +} + +static guint32 +get_allowed_inhibit (const char *app_id) +{ + g_auto(GStrv) perms = NULL; + guint32 ret = 0; + + perms = get_permissions_sync (app_id, PERMISSION_TABLE, PERMISSION_ID); + + if (perms != NULL) + { + int i; + + for (i = 0; perms[i]; i++) + { + if (strcmp (perms[i], "logout") == 0) + ret |= INHIBIT_LOGOUT; + else if (strcmp (perms[i], "switch") == 0) + ret |= INHIBIT_USER_SWITCH; + else if (strcmp (perms[i], "suspend") == 0) + ret |= INHIBIT_SUSPEND; + else if (strcmp (perms[i], "idle") == 0) + ret |= INHIBIT_IDLE; + else + g_warning ("Unknown inhibit flag in permission store: %s", perms[i]); + } + } + else + ret = INHIBIT_ALL; /* all allowed */ + + g_debug ("Inhibit permissions for %s: %d", app_id, ret); + + return ret; +} + +static void +handle_inhibit_in_thread_func (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + Request *request = (Request *)task_data; + const char *window; + guint32 flags; + GVariant *options; + const char *app_id; + + REQUEST_AUTOLOCK (request); + + window = (const char *)g_object_get_data (G_OBJECT (request), "window"); + flags = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (request), "flags")); + options = (GVariant *)g_object_get_data (G_OBJECT (request), "options"); + + app_id = xdp_app_info_get_id (request->app_info); + flags = flags & get_allowed_inhibit (app_id); + + if (flags == 0) + return; + + g_debug ("Calling inhibit backend for %s: %d", app_id, flags); + xdp_dbus_impl_inhibit_call_inhibit (impl, + request->id, + app_id, + window, + flags, + options, + NULL, + inhibit_done, + g_object_ref (request)); +} + +static gboolean +validate_reason (const char *key, + GVariant *value, + GVariant *options, + GError **error) +{ + const char *string = g_variant_get_string (value, NULL); + + if (g_utf8_strlen (string, -1) > 256) + { + g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Not accepting overly long reasons"); + return FALSE; + } + + return TRUE; +} + +static XdpOptionKey inhibit_options[] = { + { "reason", G_VARIANT_TYPE_STRING, validate_reason } +}; + +static gboolean +handle_inhibit (XdpDbusInhibit *object, + GDBusMethodInvocation *invocation, + const char *arg_window, + guint32 arg_flags, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + g_autoptr(GError) error = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + g_autoptr(GTask) task = NULL; + GVariantBuilder opt_builder; + g_autoptr(GVariant) options = NULL; + + REQUEST_AUTOLOCK (request); + + if ((arg_flags & ~INHIBIT_ALL) != 0) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Invalid flags"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + xdp_filter_options (arg_options, &opt_builder, + inhibit_options, G_N_ELEMENTS (inhibit_options), + NULL); + + options = g_variant_ref_sink (g_variant_builder_end (&opt_builder)); + + g_object_set_data_full (G_OBJECT (request), "window", g_strdup (arg_window), g_free); + g_object_set_data (G_OBJECT (request), "flags", GUINT_TO_POINTER (arg_flags)); + g_object_set_data_full (G_OBJECT (request), "options", g_variant_ref (options), (GDestroyNotify)g_variant_unref); + + impl_request = xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, &error); + if (!impl_request) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request (request, impl_request); + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + task = g_task_new (object, NULL, NULL, NULL); + g_task_set_task_data (task, g_object_ref (request), g_object_unref); + g_task_run_in_thread (task, handle_inhibit_in_thread_func); + + xdp_dbus_inhibit_complete_inhibit (object, invocation, request->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +typedef struct _InhibitSession +{ + Session parent; + + gboolean closed; +} InhibitSession; + +typedef struct _InhibitSessionClass +{ + SessionClass parent_class; +} InhibitSessionClass; + +GType inhibit_session_get_type (void); + +G_DEFINE_TYPE (InhibitSession, inhibit_session, session_get_type ()) + +static void +inhibit_session_close (Session *session) +{ + InhibitSession *inhibit_session = (InhibitSession *)session; + + inhibit_session->closed = TRUE; + + g_debug ("inhibit session owned by '%s' closed", session->sender); +} + +static void +inhibit_session_finalize (GObject *object) +{ + G_OBJECT_CLASS (inhibit_session_parent_class)->finalize (object); +} + +static void +inhibit_session_init (InhibitSession *inhibit_session) +{ +} + +static void +inhibit_session_class_init (InhibitSessionClass *klass) +{ + GObjectClass *object_class; + SessionClass *session_class; + + object_class = G_OBJECT_CLASS (klass); + object_class->finalize = inhibit_session_finalize; + + session_class = (SessionClass *)klass; + session_class->close = inhibit_session_close; +} + +static InhibitSession * +inhibit_session_new (GVariant *options, + Request *request, + GError **error) +{ + Session *session; + const char *session_token; + GDBusInterfaceSkeleton *interface_skeleton = G_DBUS_INTERFACE_SKELETON (request); + GDBusConnection *connection = g_dbus_interface_skeleton_get_connection (interface_skeleton); + GDBusConnection *impl_connection = g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)); + const char *impl_dbus_name = g_dbus_proxy_get_name (G_DBUS_PROXY (impl)); + + session_token = lookup_session_token (options); + session = g_initable_new (inhibit_session_get_type (), NULL, error, + "sender", request->sender, + "app-id", xdp_app_info_get_id (request->app_info), + "token", session_token, + "connection", connection, + "impl-connection", impl_connection, + "impl-dbus-name", impl_dbus_name, + NULL); + + if (session) + g_debug ("inhibit session owned by '%s' created", session->sender); + + return (InhibitSession*)session; +} + +static void +create_monitor_done (GObject *source_object, + GAsyncResult *res, + gpointer data) +{ + g_autoptr(Request) request = data; + Session *session; + guint response = 2; + gboolean should_close_session; + GVariantBuilder results_builder; + g_autoptr(GError) error = NULL; + + REQUEST_AUTOLOCK (request); + + session = g_object_get_data (G_OBJECT (request), "session"); + SESSION_AUTOLOCK_UNREF (g_object_ref (session)); + g_object_set_data (G_OBJECT (request), "session", NULL); + + g_variant_builder_init (&results_builder, G_VARIANT_TYPE_VARDICT); + + if (!xdp_dbus_impl_inhibit_call_create_monitor_finish (impl, &response, res, &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("A backend call failed: %s", error->message); + should_close_session = TRUE; + goto out; + } + + if (request->exported && response == 0) + { + if (!session_export (session, &error)) + { + g_warning ("Failed to export session: %s", error->message); + response = 2; + should_close_session = TRUE; + goto out; + } + + should_close_session = FALSE; + session_register (session); + } + else + { + should_close_session = TRUE; + } + + g_variant_builder_add (&results_builder, "{sv}", + "session_handle", g_variant_new ("s", session->id)); + +out: + if (request->exported) + { + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + response, + g_variant_builder_end (&results_builder)); + request_unexport (request); + } + else + { + g_variant_builder_clear (&results_builder); + } + + if (should_close_session) + session_close (session, FALSE); +} + +static gboolean +handle_create_monitor (XdpDbusInhibit *object, + GDBusMethodInvocation *invocation, + const char *arg_window, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + g_autoptr(GError) error = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + Session *session; + + REQUEST_AUTOLOCK (request); + + impl_request = + xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, &error); + if (!impl_request) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request (request, impl_request); + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + session = (Session *)inhibit_session_new (arg_options, request, &error); + if (!session) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_object_set_data_full (G_OBJECT (request), "session", g_object_ref (session), g_object_unref); + + xdp_dbus_impl_inhibit_call_create_monitor (impl, + request->id, + session->id, + xdp_app_info_get_id (request->app_info), + arg_window, + NULL, + create_monitor_done, + g_object_ref (request)); + + xdp_dbus_inhibit_complete_create_monitor (object, invocation, request->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_query_end_response (XdpDbusInhibit *object, + GDBusMethodInvocation *invocation, + const char *session_id) +{ + g_autoptr(Session) session = lookup_session (session_id); + + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + xdp_dbus_impl_inhibit_call_query_end_response (impl, session->id, + NULL, NULL, NULL); + xdp_dbus_inhibit_complete_query_end_response (object, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + + +static void +inhibit_iface_init (XdpDbusInhibitIface *iface) +{ + iface->handle_inhibit = handle_inhibit; + iface->handle_create_monitor = handle_create_monitor; + iface->handle_query_end_response = handle_query_end_response; +} + +static void +inhibit_init (Inhibit *inhibit) +{ + xdp_dbus_inhibit_set_version (XDP_DBUS_INHIBIT (inhibit), 3); +} + +static void +inhibit_class_init (InhibitClass *klass) +{ +} + +static void +state_changed_cb (XdpDbusImplInhibit *impl, + const char *session_id, + GVariant *state, + gpointer data) +{ + GDBusConnection *connection = g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)); + g_autoptr(Session) session = lookup_session (session_id); + InhibitSession *inhibit_session = (InhibitSession *)session; + gboolean active = FALSE; + guint32 session_state = 0; + + g_variant_lookup (state, "screensaver-active", "b", &active); + g_variant_lookup (state, "session-state", "u", &session_state); + g_debug ("Received state-changed %s: screensaver-active: %d, session-state: %u", + session_id, active, session_state); + + if (inhibit_session && !inhibit_session->closed) + g_dbus_connection_emit_signal (connection, + session->sender, + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.Inhibit", + "StateChanged", + g_variant_new ("(o@a{sv})", session_id, state), + NULL); +} + +GDBusInterfaceSkeleton * +inhibit_create (GDBusConnection *connection, + const char *dbus_name) +{ + g_autoptr(GError) error = NULL; + + impl = xdp_dbus_impl_inhibit_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_NONE, + dbus_name, + "/org/freedesktop/portal/desktop", + NULL, &error); + if (impl == NULL) + { + g_warning ("Failed to create inhibit proxy: %s", error->message); + return NULL; + } + + g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (impl), G_MAXINT); + + inhibit = g_object_new (inhibit_get_type (), NULL); + + g_signal_connect (impl, "state-changed", G_CALLBACK (state_changed_cb), inhibit); + + return G_DBUS_INTERFACE_SKELETON (inhibit); +} diff --git a/src/inhibit.h b/src/inhibit.h new file mode 100644 index 0000000..886f913 --- /dev/null +++ b/src/inhibit.h @@ -0,0 +1,27 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + + +#pragma once + +#include + +GDBusInterfaceSkeleton * inhibit_create (GDBusConnection *connection, + const char *dbus_name); diff --git a/src/input-capture.c b/src/input-capture.c new file mode 100644 index 0000000..05f1cc7 --- /dev/null +++ b/src/input-capture.c @@ -0,0 +1,1199 @@ +/* + * Copyright © 2017-2018 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + */ + +#include "config.h" + +#include +#include +#include + +#include "session.h" +#include "input-capture.h" +#include "request.h" +#include "xdp-dbus.h" +#include "xdp-impl-dbus.h" +#include "xdp-utils.h" + +#define VERSION_1 1 /* Makes grep easier */ + +typedef struct _InputCapture InputCapture; +typedef struct _InputCaptureClass InputCaptureClass; + +struct _InputCapture +{ + XdpDbusInputCaptureSkeleton parent_instance; +}; + +struct _InputCaptureClass +{ + XdpDbusInputCaptureSkeletonClass parent_class; +}; + +static XdpDbusImplInputCapture *impl; +static int impl_version; +static InputCapture *input_capture; + +static GQuark quark_request_session; + +GType input_capture_get_type (void); +static void input_capture_iface_init (XdpDbusInputCaptureIface *iface); + +G_DEFINE_TYPE_WITH_CODE (InputCapture, input_capture, XDP_DBUS_TYPE_INPUT_CAPTURE_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_INPUT_CAPTURE, + input_capture_iface_init)) + +typedef enum _InputCaptureSessionState +{ + INPUT_CAPTURE_SESSION_STATE_INIT, + INPUT_CAPTURE_SESSION_STATE_ENABLED, + INPUT_CAPTURE_SESSION_STATE_ACTIVE, + INPUT_CAPTURE_SESSION_STATE_DISABLED, + INPUT_CAPTURE_SESSION_STATE_CLOSED +} InputCaptureSessionState; + +typedef struct _InputCaptureSession +{ + Session parent; + + InputCaptureSessionState state; +} InputCaptureSession; + +typedef struct _InputCaptureSessionClass +{ + SessionClass parent_class; +} InputCaptureSessionClass; + +GType input_capture_session_get_type (void); + +G_DEFINE_TYPE (InputCaptureSession, input_capture_session, session_get_type ()) + +static gboolean +is_input_capture_session (Session *session) +{ + return G_TYPE_CHECK_INSTANCE_TYPE (session, input_capture_session_get_type ()); +} + +static InputCaptureSession * +input_capture_session_new (GVariant *options, + Request *request, + GError **error) +{ + Session *session; + GDBusInterfaceSkeleton *interface_skeleton = + G_DBUS_INTERFACE_SKELETON (request); + const char *session_token; + GDBusConnection *connection = + g_dbus_interface_skeleton_get_connection (interface_skeleton); + GDBusConnection *impl_connection = + g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)); + const char *impl_dbus_name = g_dbus_proxy_get_name (G_DBUS_PROXY (impl)); + + session_token = lookup_session_token (options); + session = g_initable_new (input_capture_session_get_type (), NULL, error, + "sender", request->sender, + "app-id", xdp_app_info_get_id (request->app_info), + "token", session_token, + "connection", connection, + "impl-connection", impl_connection, + "impl-dbus-name", impl_dbus_name, + NULL); + + if (session) + g_debug ("capture input session owned by '%s' created", session->sender); + + return (InputCaptureSession*)session; +} + +static void +create_session_done (GObject *source_object, + GAsyncResult *res, + gpointer data) +{ + g_autoptr(Request) request = data; + g_autoptr(GError) error = NULL; + GVariantBuilder results_builder; + GVariant *results; + Session *session; + gboolean should_close_session; + uint32_t capabilities = 0; + uint32_t response = 2; + + REQUEST_AUTOLOCK (request); + + session = g_object_get_qdata (G_OBJECT (request), quark_request_session); + SESSION_AUTOLOCK_UNREF (g_object_ref (session)); + g_object_set_qdata (G_OBJECT (request), quark_request_session, NULL); + + g_variant_builder_init (&results_builder, G_VARIANT_TYPE_VARDICT); + + if (!xdp_dbus_impl_input_capture_call_create_session_finish (impl, + &response, + &results, + res, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("A backend call failed: %s", error->message); + should_close_session = TRUE; + goto out; + } + + if (request->exported && response == 0) + { + if (!session_export (session, &error)) + { + g_warning ("Failed to export session: %s", error->message); + response = 2; + should_close_session = TRUE; + goto out; + } + + if (!g_variant_lookup (results, "capabilities", "u", &capabilities)) + { + g_warning ("Impl did not set capabilities"); + response = 2; + should_close_session = TRUE; + goto out; + } + + should_close_session = FALSE; + session_register (session); + + g_variant_builder_add (&results_builder, "{sv}", + "capabilities", g_variant_new_uint32 (capabilities)); + g_variant_builder_add (&results_builder, "{sv}", + "session_handle", g_variant_new ("o", session->id)); + } + else + { + should_close_session = TRUE; + } + +out: + if (request->exported) + { + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + response, + g_variant_builder_end (&results_builder)); + request_unexport (request); + } + else + { + g_variant_builder_clear (&results_builder); + } + + if (should_close_session) + session_close (session, FALSE); +} + +static gboolean +validate_capabilities (const char *key, + GVariant *value, + GVariant *options, + GError **error) +{ + uint32_t types = g_variant_get_uint32 (value); + + if ((types & ~(1 | 2 | 4 | 8)) != 0) + { + g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Unsupported capability: %x", types & ~(1 | 2 | 4 | 8)); + return FALSE; + } + + return TRUE; +} + +static XdpOptionKey input_capture_create_session_options[] = { + { "capabilities", G_VARIANT_TYPE_UINT32, validate_capabilities }, +}; + +static gboolean +handle_create_session (XdpDbusInputCapture *object, + GDBusMethodInvocation *invocation, + const char *arg_parent_window, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + g_autoptr(GError) error = NULL; + GVariantBuilder options_builder; + GVariant *options; + Session *session; + + REQUEST_AUTOLOCK (request); + + impl_request = + xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, &error); + if (!impl_request) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request (request, impl_request); + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + session = (Session *)input_capture_session_new (arg_options, request, &error); + if (!session) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &options_builder, + input_capture_create_session_options, + G_N_ELEMENTS (input_capture_create_session_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + options = g_variant_builder_end (&options_builder); + + g_object_set_qdata_full (G_OBJECT (request), + quark_request_session, + g_object_ref (session), + g_object_unref); + + xdp_dbus_impl_input_capture_call_create_session (impl, + request->id, + session->id, + xdp_app_info_get_id (request->app_info), + arg_parent_window, + options, + NULL, + create_session_done, + g_object_ref (request)); + + xdp_dbus_input_capture_complete_create_session (object, invocation, request->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +get_zones_done (GObject *source_object, + GAsyncResult *res, + gpointer data) +{ + g_autoptr(GVariant) results = NULL; + g_autoptr(Request) request = NULL; + g_autoptr(GError) error = NULL; + Session *session; + gboolean should_close_session; + uint32_t response = 2; + + request = (Request *)data; + + REQUEST_AUTOLOCK (request); + + session = g_object_get_qdata (G_OBJECT (request), quark_request_session); + SESSION_AUTOLOCK_UNREF (g_object_ref (session)); + g_object_set_qdata (G_OBJECT (request), quark_request_session, NULL); + + if (!xdp_dbus_impl_input_capture_call_get_zones_finish (impl, + &response, + &results, + res, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("A backend call failed: %s", error->message); + } + + should_close_session = !request->exported || response != 0; + + if (request->exported) + { + if (response != 0) + { + GVariantBuilder results_builder; + + g_variant_builder_init (&results_builder, G_VARIANT_TYPE_VARDICT); + results = g_variant_builder_end (&results_builder); + } + + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), response, results); + request_unexport (request); + } + + if (should_close_session) + { + session_close (session, TRUE); + } +} + +static XdpOptionKey input_capture_get_zones_options[] = { +}; + +static gboolean +handle_get_zones (XdpDbusInputCapture *object, + GDBusMethodInvocation *invocation, + const char *arg_session_handle, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + InputCaptureSession *input_capture_session; + g_autoptr(GError) error = NULL; + GVariantBuilder options_builder; + GVariant *options; + Session *session; + + REQUEST_AUTOLOCK (request); + + session = acquire_session (arg_session_handle, request); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + if (!is_input_capture_session (session)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + input_capture_session = (InputCaptureSession *)session; + + switch (input_capture_session->state) + { + case INPUT_CAPTURE_SESSION_STATE_INIT: + case INPUT_CAPTURE_SESSION_STATE_ENABLED: + case INPUT_CAPTURE_SESSION_STATE_ACTIVE: + case INPUT_CAPTURE_SESSION_STATE_DISABLED: + break; + case INPUT_CAPTURE_SESSION_STATE_CLOSED: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + impl_request = + xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, &error); + if (!impl_request) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request (request, impl_request); + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &options_builder, + input_capture_get_zones_options, + G_N_ELEMENTS (input_capture_get_zones_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + options = g_variant_builder_end (&options_builder); + + g_object_set_qdata_full (G_OBJECT (request), + quark_request_session, + g_object_ref (session), + g_object_unref); + + xdp_dbus_impl_input_capture_call_get_zones (impl, + request->id, + arg_session_handle, + xdp_app_info_get_id (request->app_info), + options, + NULL, + get_zones_done, + g_object_ref (request)); + + xdp_dbus_input_capture_complete_get_zones (object, invocation, request->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +set_pointer_barriers_done (GObject *source_object, + GAsyncResult *res, + gpointer data) +{ + g_autoptr(GVariant) results = NULL; + g_autoptr(Request) request = NULL; + g_autoptr(GError) error = NULL; + gboolean should_close_session; + Session *session; + uint32_t response = 2; + + request = (Request *)data; + + REQUEST_AUTOLOCK (request); + + session = g_object_get_qdata (G_OBJECT (request), quark_request_session); + SESSION_AUTOLOCK_UNREF (g_object_ref (session)); + g_object_set_qdata (G_OBJECT (request), quark_request_session, NULL); + + if (!xdp_dbus_impl_input_capture_call_set_pointer_barriers_finish (impl, + &response, + &results, + res, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("A backend call failed: %s", error->message); + } + + should_close_session = !request->exported || response != 0; + + if (request->exported) + { + if (response != 0) + { + GVariantBuilder results_builder; + + g_variant_builder_init (&results_builder, G_VARIANT_TYPE_VARDICT); + results = g_variant_builder_end (&results_builder); + } + + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), response, results); + request_unexport (request); + } + + if (should_close_session) + session_close (session, TRUE); +} + +static XdpOptionKey input_capture_set_pointer_barriers_options[] = { +}; + +static gboolean +handle_set_pointer_barriers (XdpDbusInputCapture *object, + GDBusMethodInvocation *invocation, + const char *arg_session_handle, + GVariant *arg_options, + GVariant *arg_barriers, + uint32_t arg_zone_set) +{ + Request *request = request_from_invocation (invocation); + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + InputCaptureSession *input_capture_session; + g_autoptr(GError) error = NULL; + GVariantBuilder options_builder; + GVariant *options; + Session *session; + + REQUEST_AUTOLOCK (request); + + session = acquire_session (arg_session_handle, request); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + if (!is_input_capture_session (session)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + input_capture_session = (InputCaptureSession *)session; + + switch (input_capture_session->state) + { + case INPUT_CAPTURE_SESSION_STATE_INIT: + case INPUT_CAPTURE_SESSION_STATE_ENABLED: + case INPUT_CAPTURE_SESSION_STATE_ACTIVE: + case INPUT_CAPTURE_SESSION_STATE_DISABLED: + break; + case INPUT_CAPTURE_SESSION_STATE_CLOSED: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + impl_request = + xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, &error); + if (!impl_request) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request (request, impl_request); + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &options_builder, + input_capture_set_pointer_barriers_options, + G_N_ELEMENTS (input_capture_set_pointer_barriers_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + options = g_variant_builder_end (&options_builder); + + g_object_set_qdata_full (G_OBJECT (request), + quark_request_session, + g_object_ref (session), + g_object_unref); + + xdp_dbus_impl_input_capture_call_set_pointer_barriers (impl, + request->id, + arg_session_handle, + xdp_app_info_get_id (request->app_info), + options, + g_variant_ref (arg_barriers), /* FIXME: validation? */ + arg_zone_set, /* FIXME: validation? */ + NULL, + set_pointer_barriers_done, + g_object_ref (request)); + + xdp_dbus_input_capture_complete_set_pointer_barriers (object, invocation, request->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static XdpOptionKey input_capture_enable_options[] = { +}; + +static gboolean +handle_enable (XdpDbusInputCapture *object, + GDBusMethodInvocation *invocation, + const char *arg_session_handle, + GVariant *arg_options) +{ + Call *call = call_from_invocation (invocation); + Session *session; + InputCaptureSession *input_capture_session; + g_autoptr(GError) error = NULL; + GVariantBuilder options_builder; + + session = acquire_session_from_call (arg_session_handle, call); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + if (!is_input_capture_session (session)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + input_capture_session = (InputCaptureSession *)session; + + switch (input_capture_session->state) + { + case INPUT_CAPTURE_SESSION_STATE_INIT: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Not connected to EIS"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + case INPUT_CAPTURE_SESSION_STATE_ENABLED: + case INPUT_CAPTURE_SESSION_STATE_ACTIVE: + case INPUT_CAPTURE_SESSION_STATE_DISABLED: + break; + case INPUT_CAPTURE_SESSION_STATE_CLOSED: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + + if (!xdp_filter_options (arg_options, &options_builder, + input_capture_enable_options, + G_N_ELEMENTS (input_capture_enable_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + input_capture_session->state = INPUT_CAPTURE_SESSION_STATE_ENABLED; + + /* Let's be lenient and make Enable() a noop for anything but a disabled + * session. + */ + switch (input_capture_session->state) + { + case INPUT_CAPTURE_SESSION_STATE_INIT: /* ignore, handled above */ + g_assert_not_reached (); + case INPUT_CAPTURE_SESSION_STATE_ENABLED: + case INPUT_CAPTURE_SESSION_STATE_ACTIVE: + break; + case INPUT_CAPTURE_SESSION_STATE_DISABLED: + input_capture_session->state = INPUT_CAPTURE_SESSION_STATE_ENABLED; + break; + case INPUT_CAPTURE_SESSION_STATE_CLOSED: /* ignore, handled above */ + g_assert_not_reached (); + } + + xdp_dbus_impl_input_capture_call_enable (impl, + arg_session_handle, + xdp_app_info_get_id (call->app_info), + g_variant_builder_end (&options_builder), + NULL, + NULL, + NULL); + + xdp_dbus_input_capture_complete_enable (object, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static XdpOptionKey input_capture_disable_options[] = { +}; + +static gboolean +handle_disable (XdpDbusInputCapture *object, + GDBusMethodInvocation *invocation, + const char *arg_session_handle, + GVariant *arg_options) +{ + Call *call = call_from_invocation (invocation); + Session *session; + InputCaptureSession *input_capture_session; + g_autoptr(GError) error = NULL; + GVariantBuilder options_builder; + + session = acquire_session_from_call (arg_session_handle, call); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + if (!is_input_capture_session (session)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + input_capture_session = (InputCaptureSession *)session; + + switch (input_capture_session->state) + { + case INPUT_CAPTURE_SESSION_STATE_INIT: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Not connected to EIS"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + case INPUT_CAPTURE_SESSION_STATE_ENABLED: + case INPUT_CAPTURE_SESSION_STATE_ACTIVE: + case INPUT_CAPTURE_SESSION_STATE_DISABLED: + break; + case INPUT_CAPTURE_SESSION_STATE_CLOSED: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &options_builder, + input_capture_disable_options, + G_N_ELEMENTS (input_capture_disable_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + /* We need to be lenient, a caller may call Disable() before processing a + * Disabled signal. So we pretend everything's ok but only + * update our internal state in the right transitions. + */ + switch (input_capture_session->state) + { + case INPUT_CAPTURE_SESSION_STATE_INIT: /* ignore, handled above */ + g_assert_not_reached (); + case INPUT_CAPTURE_SESSION_STATE_ENABLED: + case INPUT_CAPTURE_SESSION_STATE_ACTIVE: + input_capture_session->state = INPUT_CAPTURE_SESSION_STATE_DISABLED; + break; + case INPUT_CAPTURE_SESSION_STATE_DISABLED: + break; + case INPUT_CAPTURE_SESSION_STATE_CLOSED: /* ignore, handled above */ + g_assert_not_reached (); + } + + xdp_dbus_impl_input_capture_call_disable (impl, + arg_session_handle, + xdp_app_info_get_id (call->app_info), + g_variant_builder_end (&options_builder), + NULL, + NULL, + NULL); + + xdp_dbus_input_capture_complete_disable (object, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static XdpOptionKey input_capture_release_options[] = { + { "cursor_position", (const GVariantType *)"(dd)", NULL }, + { "activation_id", G_VARIANT_TYPE_UINT32, NULL }, +}; + +static gboolean +handle_release (XdpDbusInputCapture *object, + GDBusMethodInvocation *invocation, + const char *arg_session_handle, + GVariant *arg_options) +{ + Call *call = call_from_invocation (invocation); + Session *session; + InputCaptureSession *input_capture_session; + g_autoptr(GError) error = NULL; + GVariantBuilder options_builder; + + session = acquire_session_from_call (arg_session_handle, call); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + if (!is_input_capture_session (session)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + input_capture_session = (InputCaptureSession *)session; + + switch (input_capture_session->state) + { + case INPUT_CAPTURE_SESSION_STATE_INIT: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Not connected to EIS"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + case INPUT_CAPTURE_SESSION_STATE_ENABLED: + case INPUT_CAPTURE_SESSION_STATE_ACTIVE: + case INPUT_CAPTURE_SESSION_STATE_DISABLED: + break; + case INPUT_CAPTURE_SESSION_STATE_CLOSED: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &options_builder, + input_capture_release_options, + G_N_ELEMENTS (input_capture_release_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + /* We need to be lenient, a caller may call Release() before processing a + * Deactivated/Disabled signal. So we pretend everything's ok but only + * update our internal state in the right transitions. + */ + switch (input_capture_session->state) + { + case INPUT_CAPTURE_SESSION_STATE_INIT: /* ignore, handled above */ + g_assert_not_reached (); + case INPUT_CAPTURE_SESSION_STATE_ENABLED: + break; + case INPUT_CAPTURE_SESSION_STATE_ACTIVE: + input_capture_session->state = INPUT_CAPTURE_SESSION_STATE_ENABLED; + break; + case INPUT_CAPTURE_SESSION_STATE_DISABLED: + break; + case INPUT_CAPTURE_SESSION_STATE_CLOSED: /* ignore, handled above */ + g_assert_not_reached (); + } + + xdp_dbus_impl_input_capture_call_release (impl, + arg_session_handle, + xdp_app_info_get_id (call->app_info), + g_variant_builder_end (&options_builder), + NULL, + NULL, + NULL); + + xdp_dbus_input_capture_complete_release (object, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_connect_to_eis (XdpDbusInputCapture *object, + GDBusMethodInvocation *invocation, + GUnixFDList *in_fd_list, + const char *arg_session_handle, + GVariant *arg_options) +{ + Call *call = call_from_invocation (invocation); + Session *session; + InputCaptureSession *input_capture_session; + g_autoptr(GUnixFDList) out_fd_list = NULL; + g_autoptr(GError) error = NULL; + GVariantBuilder empty; + GVariant *fd; + + session = acquire_session_from_call (arg_session_handle, call); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + if (!is_input_capture_session (session)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + input_capture_session = (InputCaptureSession *)session; + + switch (input_capture_session->state) + { + case INPUT_CAPTURE_SESSION_STATE_INIT: + break; + case INPUT_CAPTURE_SESSION_STATE_ENABLED: + case INPUT_CAPTURE_SESSION_STATE_ACTIVE: + case INPUT_CAPTURE_SESSION_STATE_DISABLED: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Already connected"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + case INPUT_CAPTURE_SESSION_STATE_CLOSED: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&empty, G_VARIANT_TYPE_VARDICT); + + if (!xdp_dbus_impl_input_capture_call_connect_to_eis_sync (impl, + arg_session_handle, + xdp_app_info_get_id (call->app_info), + g_variant_builder_end (&empty), + in_fd_list, + &fd, + &out_fd_list, + NULL, + &error)) + { + g_warning ("Failed to ConnectToEIS: %s", error->message); + out_fd_list = g_unix_fd_list_new (); + } + + input_capture_session->state = INPUT_CAPTURE_SESSION_STATE_DISABLED; + + xdp_dbus_input_capture_complete_connect_to_eis (object, invocation, out_fd_list, fd); + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +input_capture_iface_init (XdpDbusInputCaptureIface *iface) +{ + iface->handle_create_session = handle_create_session; + iface->handle_get_zones = handle_get_zones; + iface->handle_set_pointer_barriers = handle_set_pointer_barriers; + iface->handle_connect_to_eis = handle_connect_to_eis; + iface->handle_enable = handle_enable; + iface->handle_disable = handle_disable; + iface->handle_release = handle_release; +} + +static void +pass_signal (XdpDbusImplInputCapture *impl, + const char *signal_name, + const char *session_id, + GVariant *options, + gpointer data) +{ + GDBusConnection *connection = g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)); + g_autoptr(Session) session = lookup_session (session_id); + + g_dbus_connection_emit_signal (connection, + session->sender, + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.InputCapture", + signal_name, + g_variant_new ("(o@a{sv})", session_id, options), + NULL); +} + +static void +on_disabled_cb (XdpDbusImplInputCapture *impl, + const char *session_id, + GVariant *options, + gpointer data) +{ + g_autoptr(Session) session = lookup_session (session_id); + InputCaptureSession *input_capture_session; + + if (!is_input_capture_session (session)) + { + g_critical ("Invalid session type for signal"); + return; + } + + input_capture_session = (InputCaptureSession*)session; + + switch (input_capture_session->state) + { + case INPUT_CAPTURE_SESSION_STATE_INIT: + break; + case INPUT_CAPTURE_SESSION_STATE_ENABLED: + case INPUT_CAPTURE_SESSION_STATE_ACTIVE: + pass_signal (impl, "Disabled", session_id, options, data); + input_capture_session->state = INPUT_CAPTURE_SESSION_STATE_DISABLED; + break; + case INPUT_CAPTURE_SESSION_STATE_DISABLED: + break; + case INPUT_CAPTURE_SESSION_STATE_CLOSED: + break; + } +} + +static void +on_activated_cb (XdpDbusImplInputCapture *impl, + const char *session_id, + GVariant *options, + gpointer data) +{ + g_autoptr(Session) session = lookup_session (session_id); + InputCaptureSession *input_capture_session; + + if (!is_input_capture_session (session)) + { + g_critical ("Invalid session type for signal"); + return; + } + + input_capture_session = (InputCaptureSession*)session; + + switch (input_capture_session->state) + { + case INPUT_CAPTURE_SESSION_STATE_INIT: + break; + case INPUT_CAPTURE_SESSION_STATE_ENABLED: + pass_signal (impl, "Activated", session_id, options, data); + input_capture_session->state = INPUT_CAPTURE_SESSION_STATE_ACTIVE; + break; + case INPUT_CAPTURE_SESSION_STATE_ACTIVE: + case INPUT_CAPTURE_SESSION_STATE_DISABLED: + case INPUT_CAPTURE_SESSION_STATE_CLOSED: + break; + } +} + +static void +on_deactivated_cb (XdpDbusImplInputCapture *impl, + const char *session_id, + GVariant *options, + gpointer data) +{ + g_autoptr(Session) session = lookup_session (session_id); + InputCaptureSession *input_capture_session; + + if (!is_input_capture_session (session)) + { + g_critical ("Invalid session type for signal"); + return; + } + + input_capture_session = (InputCaptureSession*)session; + + switch (input_capture_session->state) + { + case INPUT_CAPTURE_SESSION_STATE_INIT: + case INPUT_CAPTURE_SESSION_STATE_ENABLED: + break; + case INPUT_CAPTURE_SESSION_STATE_ACTIVE: + pass_signal (impl, "Deactivated", session_id, options, data); + input_capture_session->state = INPUT_CAPTURE_SESSION_STATE_ACTIVE; + break; + case INPUT_CAPTURE_SESSION_STATE_DISABLED: + case INPUT_CAPTURE_SESSION_STATE_CLOSED: + break; + } +} + +static void +input_capture_init (InputCapture *input_capture) +{ + unsigned int supported_capabilities; + + xdp_dbus_input_capture_set_version (XDP_DBUS_INPUT_CAPTURE (input_capture), VERSION_1); + + supported_capabilities = + xdp_dbus_impl_input_capture_get_supported_capabilities (impl); + xdp_dbus_input_capture_set_supported_capabilities (XDP_DBUS_INPUT_CAPTURE (input_capture), + supported_capabilities); + + g_signal_connect (impl, "disabled", G_CALLBACK (on_disabled_cb), input_capture); + g_signal_connect (impl, "activated", G_CALLBACK (on_activated_cb), input_capture); + g_signal_connect (impl, "deactivated", G_CALLBACK (on_deactivated_cb), input_capture); +} + +static void +input_capture_class_init (InputCaptureClass *klass) +{ + quark_request_session = + g_quark_from_static_string ("-xdp-request-capture-input-session"); +} + +static void +input_capture_session_close (Session *session) +{ + InputCaptureSession *input_capture_session = (InputCaptureSession *)session; + + input_capture_session->state = INPUT_CAPTURE_SESSION_STATE_CLOSED; + + g_debug ("screen cast session owned by '%s' closed", session->sender); +} + +static void +input_capture_session_finalize (GObject *object) +{ + G_OBJECT_CLASS (input_capture_session_parent_class)->finalize (object); +} + +static void +input_capture_session_init (InputCaptureSession *input_capture_session) +{ +} + +static void +input_capture_session_class_init (InputCaptureSessionClass *klass) +{ + GObjectClass *object_class; + SessionClass *session_class; + + object_class = G_OBJECT_CLASS (klass); + object_class->finalize = input_capture_session_finalize; + + session_class = (SessionClass *)klass; + session_class->close = input_capture_session_close; +} + +GDBusInterfaceSkeleton * +input_capture_create (GDBusConnection *connection, + const char *dbus_name) +{ + g_autoptr(GError) error = NULL; + + impl = xdp_dbus_impl_input_capture_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_NONE, + dbus_name, + DESKTOP_PORTAL_OBJECT_PATH, + NULL, + &error); + if (impl == NULL) + { + g_warning ("Failed to create capture input proxy: %s", error->message); + return NULL; + } + + impl_version = xdp_dbus_impl_input_capture_get_version (impl); + + g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (impl), G_MAXINT); + + input_capture = g_object_new (input_capture_get_type (), NULL); + + return G_DBUS_INTERFACE_SKELETON (input_capture); +} diff --git a/src/input-capture.h b/src/input-capture.h new file mode 100644 index 0000000..7db7a7b --- /dev/null +++ b/src/input-capture.h @@ -0,0 +1,25 @@ +/* + * Copyright © 2022 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + */ + +#pragma once + +#include +#include + +GDBusInterfaceSkeleton * input_capture_create (GDBusConnection *connection, + const char *dbus_name); diff --git a/src/location.c b/src/location.c new file mode 100644 index 0000000..b56772c --- /dev/null +++ b/src/location.c @@ -0,0 +1,742 @@ +/* + * Copyright © 2018 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +#include "config.h" + +#include + +#include + +#include +#include + +#include "location.h" +#include "request.h" +#include "permissions.h" +#include "xdp-dbus.h" +#include "xdp-utils.h" +#include "session.h" +#include "geoclue-dbus.h" +#include + +static GClueAccuracyLevel gclue_accuracy_level_from_string (const char *str); +static const char * gclue_accuracy_level_to_string (GClueAccuracyLevel level); + +static GQuark quark_request_session; +extern gboolean opt_verbose; + +typedef enum { + LOCATION_SESSION_STATE_INIT, + LOCATION_SESSION_STATE_STARTING, + LOCATION_SESSION_STATE_STARTED, + LOCATION_SESSION_STATE_CLOSED +} LocationSessionState; + +typedef struct +{ + Session parent; + + LocationSessionState state; + + guint distance_threshold; + guint time_threshold; + guint accuracy; + + GeoclueClient *client; +} LocationSession; + +typedef struct +{ + SessionClass parent_class; +} LocationSessionClass; + +GType location_session_get_type (void); + +G_DEFINE_TYPE (LocationSession, location_session, session_get_type ()) + +static void +location_session_init (LocationSession *session) +{ + session->distance_threshold = 0; + session->time_threshold = 0; + session->accuracy = GCLUE_ACCURACY_LEVEL_EXACT; +} + +static void +location_session_close (Session *session) +{ + LocationSession *loc_session = (LocationSession *)session; + + loc_session->state = LOCATION_SESSION_STATE_CLOSED; + + if (loc_session->client) + geoclue_client_call_stop (loc_session->client, NULL, NULL, NULL); + + g_debug ("location session '%s' closed", session->id); +} + +static void +location_session_finalize (GObject *object) +{ + LocationSession *loc_session = (LocationSession *)object; + + g_clear_object (&loc_session->client); + + G_OBJECT_CLASS (location_session_parent_class)->finalize (object); +} + +static void +location_session_class_init (LocationSessionClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + SessionClass *session_class = (SessionClass *)klass; + + object_class->finalize = location_session_finalize; + + session_class->close = location_session_close; +} + +static LocationSession * +location_session_new (GVariant *options, + GDBusMethodInvocation *invocation, + GError **error) +{ + GDBusConnection *connection = g_dbus_method_invocation_get_connection (invocation); + const gchar *sender = g_dbus_method_invocation_get_sender (invocation); + XdpAppInfo *app_info = xdp_invocation_lookup_app_info_sync (invocation, NULL, NULL); + Session *session; + + session = g_initable_new (location_session_get_type (), NULL, error, + "sender", sender, + "app-id", xdp_app_info_get_id (app_info), + "token", lookup_session_token (options), + "connection", connection, + NULL); + + if (session) + g_debug ("location session '%s' created", session->id); + + return (LocationSession*)session; +} + +/*** GeoClue integration ***/ + +static void +location_updated (GeoclueClient *client, + const char *old_location, + const char *new_location, + gpointer data) +{ + Session *session = data; + g_autoptr(GVariant) ret = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) dict = NULL; + + g_debug ("GeoClue client ::LocationUpdated %s -> %s\n", old_location, new_location); + + if (strcmp (new_location, "/") == 0) + return; + + ret = g_dbus_connection_call_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (client)), + "org.freedesktop.GeoClue2", + new_location, + "org.freedesktop.DBus.Properties", + "GetAll", + g_variant_new ("(s)", "org.freedesktop.GeoClue2.Location"), + G_VARIANT_TYPE ("(a{sv})"), + 0, -1, NULL, &error); + if (ret == NULL) + { + g_warning ("Failed to get location properties: %s", error->message); + return; + } + + g_variant_get (ret, "(@a{sv})", &dict); + + if (opt_verbose) + { + g_autofree char *a = g_variant_print (dict, FALSE); + g_debug ("location data: %s\n", a); + } + + if (!g_dbus_connection_emit_signal (session->connection, + session->sender, + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.Location", + "LocationUpdated", + g_variant_new ("(o@a{sv})", session->id, dict), + &error)) + { + g_warning ("Failed to emit LocationUpdated signal: %s", error->message); + } +} + +static gboolean +location_session_start (LocationSession *loc_session) +{ + g_autoptr(GDBusConnection) system_bus = NULL; + g_autoptr(GVariant) ret = NULL; + g_autoptr(GError) error = NULL; + char *client_id; + + /* FIXME: this is all ugly and sync */ + + system_bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, NULL); + ret = g_dbus_connection_call_sync (system_bus, + "org.freedesktop.GeoClue2", + "/org/freedesktop/GeoClue2/Manager", + "org.freedesktop.GeoClue2.Manager", + "GetClient", + NULL, + G_VARIANT_TYPE ("(o)"), + 0, -1, NULL, &error); + if (ret == NULL) + { + g_warning ("Failed to get GeoClue client: %s", error->message); + loc_session->state = LOCATION_SESSION_STATE_CLOSED; + return FALSE; + } + + g_variant_get (ret, "(o)", &client_id); + + loc_session->client = geoclue_client_proxy_new_sync (system_bus, + G_DBUS_PROXY_FLAGS_NONE, + "org.freedesktop.GeoClue2", + client_id, + NULL, + &error); + if (loc_session->client == NULL) + { + g_warning ("Failed to get GeoClue client: %s", error->message); + loc_session->state = LOCATION_SESSION_STATE_CLOSED; + return FALSE; + } + + g_debug ("location session '%s', GeoClue client '%s'", ((Session*)loc_session)->id, client_id); + g_debug ("location session '%s', distance-threshold %d, time-threshold %d, accuracy %s", + ((Session *)loc_session)->id, + loc_session->distance_threshold, + loc_session->time_threshold, + gclue_accuracy_level_to_string (loc_session->accuracy)); + + g_object_set (loc_session->client, + "desktop-id", "xdg-desktop-portal", + "distance-threshold", loc_session->distance_threshold, + "time-threshold", loc_session->time_threshold, + "requested-accuracy-level", loc_session->accuracy, + NULL); + + g_signal_connect (loc_session->client, "location-updated", + G_CALLBACK (location_updated), loc_session); + + if (!geoclue_client_call_start_sync (loc_session->client, NULL, &error)) + { + g_warning ("Starting GeoClue client failed: %s", error->message); + loc_session->state = LOCATION_SESSION_STATE_CLOSED; + g_clear_object (&loc_session->client); + return FALSE; + } + + g_debug ("GeoClue client '%s' started", client_id); + + loc_session->state = LOCATION_SESSION_STATE_STARTED; + g_debug ("location session '%s' started", ((Session*)loc_session)->id); + + return TRUE; +} + +/*** Permission handling ***/ + +/* We use a table named 'location' with a single row with ID 'location'. + * The permissions string for each application entry consists of + * the allowed accuracy and the last-use timestamp (using monotonic time) + * Example: + * + * location + * location + * org.gnome.PortalTest CITY,1234131441 + * org.gnome.Todo EXACT,00909313134 + * org.gnome.Polari NONE,0 + * + * When no entry is found, we ask the user whether he wants to grant + * access, and use EXACT as the accuracy. + */ + +#define PERMISSION_TABLE "location" +#define PERMISSION_ID "location" + +static struct { const char *name; GClueAccuracyLevel level; } accuracy_levels[] = { + { "NONE", GCLUE_ACCURACY_LEVEL_NONE }, + { "COUNTRY", GCLUE_ACCURACY_LEVEL_COUNTRY }, + { "CITY", GCLUE_ACCURACY_LEVEL_CITY }, + { "NEIGHBORHOOD", GCLUE_ACCURACY_LEVEL_NEIGHBORHOOD }, + { "STREET", GCLUE_ACCURACY_LEVEL_STREET }, + { "EXACT", GCLUE_ACCURACY_LEVEL_EXACT } +}; + +static GClueAccuracyLevel +gclue_accuracy_level_from_string (const char *str) +{ + int i; + + for (i = 0; i < G_N_ELEMENTS (accuracy_levels); i++) + { + if (g_str_equal (accuracy_levels[i].name, str)) + return accuracy_levels[i].level; + } + + g_warning ("Unknown accuracy level: %s", str); + return GCLUE_ACCURACY_LEVEL_NONE; +} + +static const char * +gclue_accuracy_level_to_string (GClueAccuracyLevel level) +{ + int i; + + for (i = 0; i < G_N_ELEMENTS (accuracy_levels); i++) + { + if (accuracy_levels[i].level == level) + return accuracy_levels[i].name; + } + + g_warning ("Unknown accuracy level: %d", level); + return "NONE"; +} + +static gboolean +get_location_permissions (XdpAppInfo *app_info, + GClueAccuracyLevel *accuracy, + gint64 *last_used) +{ + const char *app_id = xdp_app_info_get_id (app_info); + g_auto(GStrv) perms = NULL; + + if (xdp_app_info_is_host (app_info)) + { + /* unsandboxed */ + *accuracy = GCLUE_ACCURACY_LEVEL_EXACT; + *last_used = 0; + return TRUE; + } + + g_debug ("Getting location permissions for '%s'", app_id); + + perms = get_permissions_sync (app_id, PERMISSION_TABLE, PERMISSION_ID); + + if (perms == NULL) + return FALSE; + + if (g_strv_length ((char **)perms) < 2) + { + g_warning ("Wrong permission format"); + return FALSE; + } + + *accuracy = gclue_accuracy_level_from_string (perms[0]); + *last_used = g_ascii_strtoll (perms[1], NULL, 10); + + g_debug ("got permission store accuracy: %s -> %d", perms[0], *accuracy); + + return TRUE; +} + +static void +set_location_permissions (const char *app_id, + GClueAccuracyLevel accuracy, + gint64 timestamp) +{ + g_autofree char *date = NULL; + const char *permissions[3]; + + if (app_id == NULL) + return; + + date = g_strdup_printf ("%" G_GINT64_FORMAT, timestamp); + permissions[0] = gclue_accuracy_level_to_string (accuracy); + permissions[1] = (const char *)date; + permissions[2] = NULL; + + g_debug ("set permission store accuracy: %d -> %s", accuracy, permissions[0]); + + set_permissions_sync (app_id, PERMISSION_TABLE, PERMISSION_ID, permissions); +} + +/*** Location boilerplace ***/ + +typedef struct +{ + XdpDbusLocationSkeleton parent_instance; +} Location; + +typedef struct +{ + XdpDbusLocationSkeletonClass parent_class; +} LocationClass; + +static Location *location; +static XdpDbusImplAccess *access_impl; +static XdpDbusImplLockdown *lockdown; + +GType location_get_type (void) G_GNUC_CONST; +static void location_iface_init (XdpDbusLocationIface *iface); + +G_DEFINE_TYPE_WITH_CODE (Location, location, XDP_DBUS_TYPE_LOCATION_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_LOCATION, location_iface_init)) + +/*** CreateSession ***/ + +static gboolean +handle_create_session (XdpDbusLocation *object, + GDBusMethodInvocation *invocation, + GVariant *arg_options) +{ + g_autoptr(GError) error = NULL; + LocationSession *session; + guint threshold; + guint accuracy; + + if (xdp_dbus_impl_lockdown_get_disable_location (lockdown)) + { + g_debug ("Location services disabled"); + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Location services disabled"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + session = location_session_new (arg_options, invocation, &error); + if (!session) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + if (g_variant_lookup (arg_options, "distance-threshold", "u", &threshold)) + session->distance_threshold = threshold; + if (g_variant_lookup (arg_options, "time-threshold", "u", &threshold)) + session->time_threshold = threshold; + if (g_variant_lookup (arg_options, "accuracy", "u", &accuracy)) + { + if (accuracy == 0) + session->accuracy = GCLUE_ACCURACY_LEVEL_NONE; + else if (accuracy == 1) + session->accuracy = GCLUE_ACCURACY_LEVEL_COUNTRY; + else if (accuracy == 2) + session->accuracy = GCLUE_ACCURACY_LEVEL_CITY; + else if (accuracy == 3) + session->accuracy = GCLUE_ACCURACY_LEVEL_NEIGHBORHOOD; + else if (accuracy == 4) + session->accuracy = GCLUE_ACCURACY_LEVEL_STREET; + else if (accuracy == 5) + session->accuracy = GCLUE_ACCURACY_LEVEL_EXACT; + else + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Invalid accuracy level"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + } + + if (!session_export ((Session *)session, &error)) + { + g_warning ("Failed to export session: %s", error->message); + session_close ((Session *)session, FALSE); + } + else + { + g_debug ("CreateSession new session '%s'", ((Session *)session)->id); + session_register ((Session *)session); + } + + xdp_dbus_location_complete_create_session (object, invocation, ((Session *)session)->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +/*** Start ***/ + +static void +handle_start_in_thread_func (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + Request *request = (Request *)task_data; + const char *parent_window; + const char *app_id; + gint64 last_used = 0; + g_autoptr(GError) error = NULL; + guint response = 2; + Session *session; + LocationSession *loc_session; + GClueAccuracyLevel accuracy; + + REQUEST_AUTOLOCK (request); + + session = g_object_get_qdata (G_OBJECT (request), quark_request_session); + SESSION_AUTOLOCK_UNREF (g_object_ref (session)); + g_object_set_qdata (G_OBJECT (request), quark_request_session, NULL); + loc_session = (LocationSession *)session; + + parent_window = (const char *)g_object_get_data (G_OBJECT (request), "parent-window"); + + app_id = xdp_app_info_get_id (request->app_info); + + if (!get_location_permissions (request->app_info, &accuracy, &last_used)) + { + guint access_response = 2; + g_autoptr(GVariant) access_results = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + GVariantBuilder access_opt_builder; + g_autofree char *title = NULL; + g_autofree char *subtitle = NULL; + const char *body; + + impl_request = xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (access_impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (access_impl)), + request->id, + NULL, NULL); + + request_set_impl_request (request, impl_request); + + g_variant_builder_init (&access_opt_builder, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add (&access_opt_builder, "{sv}", + "deny_label", g_variant_new_string (_("Deny Access"))); + g_variant_builder_add (&access_opt_builder, "{sv}", + "grant_label", g_variant_new_string (_("Grant Access"))); + g_variant_builder_add (&access_opt_builder, "{sv}", + "icon", g_variant_new_string ("find-location-symbolic")); + + if (g_str_equal (app_id, "")) + { + title = g_strdup (_("Grant Access to Your Location?")); + subtitle = g_strdup (_("An application wants to use your location.")); + } + else + { + g_autofree char *id = NULL; + g_autoptr(GDesktopAppInfo) info = NULL; + const char *name; + + id = g_strconcat (app_id, ".desktop", NULL); + info = g_desktop_app_info_new (id); + name = g_app_info_get_display_name (G_APP_INFO (info)); + + title = g_strdup_printf (_("Give %s Access to Your Location?"), name); + if (g_desktop_app_info_has_key (info, "X-Geoclue-Reason")) + subtitle = g_desktop_app_info_get_string (info, "X-Geoclue-Reason"); + else + subtitle = g_strdup_printf (_("%s wants to use your location."), name); + } + + body = _("Location access can be changed at any time from the privacy settings."); + + if (!xdp_dbus_impl_access_call_access_dialog_sync (access_impl, + request->id, + app_id, + parent_window, + title, + subtitle, + body, + g_variant_builder_end (&access_opt_builder), + &access_response, + &access_results, + NULL, + &error)) + { + g_warning ("Failed to show access dialog: %s", error->message); + goto out; + } + + request_set_impl_request (request, NULL); + + accuracy = (access_response == 0) ? GCLUE_ACCURACY_LEVEL_EXACT : GCLUE_ACCURACY_LEVEL_NONE; + } + + if (accuracy != GCLUE_ACCURACY_LEVEL_NONE) + last_used = g_get_monotonic_time (); + + set_location_permissions (app_id, accuracy, last_used); + + if (accuracy == GCLUE_ACCURACY_LEVEL_NONE) + { + response = 1; + goto out; + } + + if (accuracy < loc_session->accuracy) + { + g_debug ("Lowering requested accuracy from %s to %s", + gclue_accuracy_level_to_string (loc_session->accuracy), + gclue_accuracy_level_to_string (accuracy)); + loc_session->accuracy = accuracy; + } + + if (location_session_start ((LocationSession*)session)) + response = 0; + else + response = 2; + +out: + if (request->exported) + { + GVariantBuilder opt_builder; + + g_debug ("sending response: %d", response); + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + response, + g_variant_builder_end (&opt_builder)); + request_unexport (request); + } + + if (response != 0) + { + g_debug ("closing session"); + session_close ((Session *)session, FALSE); + } +} + +static gboolean +handle_start (XdpDbusLocation *object, + GDBusMethodInvocation *invocation, + const char *arg_session_handle, + const char *arg_parent_window, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + Session *session; + LocationSession *loc_session; + g_autoptr(GTask) task = NULL; + + if (xdp_dbus_impl_lockdown_get_disable_location (lockdown)) + { + g_debug ("Location services disabled"); + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Location services disabled"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + REQUEST_AUTOLOCK (request); + + session = acquire_session (arg_session_handle, request); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + loc_session = (LocationSession *)session; + switch (loc_session->state) + { + case LOCATION_SESSION_STATE_INIT: + break; + case LOCATION_SESSION_STATE_STARTING: + case LOCATION_SESSION_STATE_STARTED: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Can only start once"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + case LOCATION_SESSION_STATE_CLOSED: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + g_object_set_data_full (G_OBJECT (request), "parent-window", g_strdup (arg_parent_window), g_free); + + g_object_set_qdata_full (G_OBJECT (request), + quark_request_session, + g_object_ref (session), + g_object_unref); + + loc_session->state = LOCATION_SESSION_STATE_STARTING; + + xdp_dbus_location_complete_start (object, invocation, request->id); + + task = g_task_new (object, NULL, NULL, NULL); + g_task_set_task_data (task, g_object_ref (request), g_object_unref); + g_task_run_in_thread (task, handle_start_in_thread_func); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +/************/ + +static void +location_iface_init (XdpDbusLocationIface *iface) +{ + iface->handle_create_session = handle_create_session; + iface->handle_start = handle_start; +} + +static void +location_init (Location *location) +{ + xdp_dbus_location_set_version (XDP_DBUS_LOCATION (location), 1); +} + +static void +location_class_init (LocationClass *klass) +{ + quark_request_session = g_quark_from_static_string ("-xdp-request-location-session"); +} + +GDBusInterfaceSkeleton * +location_create (GDBusConnection *connection, + const char *dbus_name, + gpointer lockdown_proxy) +{ + g_autoptr(GError) error = NULL; + + lockdown = lockdown_proxy; + + access_impl = xdp_dbus_impl_access_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_NONE, + dbus_name, + DESKTOP_PORTAL_OBJECT_PATH, + NULL, &error); + if (access_impl == NULL) + { + g_warning ("Failed to create access proxy: %s", error->message); + return NULL; + } + + location = g_object_new (location_get_type (), NULL); + + return G_DBUS_INTERFACE_SKELETON (location); +} diff --git a/src/location.h b/src/location.h new file mode 100644 index 0000000..16a5f7a --- /dev/null +++ b/src/location.h @@ -0,0 +1,28 @@ +/* + * Copyright © 2018 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + + +#pragma once + +#include + +GDBusInterfaceSkeleton * location_create (GDBusConnection *connection, + const char *dbus_name, + gpointer lockdown); diff --git a/src/memory-monitor.c b/src/memory-monitor.c new file mode 100644 index 0000000..8808e78 --- /dev/null +++ b/src/memory-monitor.c @@ -0,0 +1,116 @@ +/* + * Copyright © 2016, 2019 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + * Bastien Nocera + */ + +#include "config.h" + +#include +#include + +#include "memory-monitor.h" +#include "request.h" +#include "xdp-dbus.h" +#include "xdp-utils.h" + +#if GLIB_CHECK_VERSION(2, 63, 3) +#define HAS_MEMORY_MONITOR 1 +#endif + +typedef struct _MemoryMonitor MemoryMonitor; +typedef struct _MemoryMonitorClass MemoryMonitorClass; + +struct _MemoryMonitor +{ + XdpDbusMemoryMonitorSkeleton parent_instance; + +#ifdef HAS_MEMORY_MONITOR + GMemoryMonitor *monitor; +#endif /* HAS_MEMORY_MONITOR */ +}; + +struct _MemoryMonitorClass +{ + XdpDbusMemoryMonitorSkeletonClass parent_class; +}; + +static MemoryMonitor *memory_monitor; + +GType memory_monitor_get_type (void) G_GNUC_CONST; +static void memory_monitor_iface_init (XdpDbusMemoryMonitorIface *iface); + +G_DEFINE_TYPE_WITH_CODE (MemoryMonitor, memory_monitor, + XDP_DBUS_TYPE_MEMORY_MONITOR_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_MEMORY_MONITOR, + memory_monitor_iface_init)); + +static void +memory_monitor_iface_init (XdpDbusMemoryMonitorIface *iface) +{ +} + +#ifdef HAS_MEMORY_MONITOR +static void +low_memory_warning_cb (GObject *object, + GMemoryMonitorWarningLevel level, + MemoryMonitor *mm) +{ + xdp_dbus_memory_monitor_emit_low_memory_warning (XDP_DBUS_MEMORY_MONITOR (mm), + level); +} +#endif /* HAS_MEMORY_MONITOR */ + +static void +memory_monitor_init (MemoryMonitor *mm) +{ +#ifdef HAS_MEMORY_MONITOR + mm->monitor = g_memory_monitor_dup_default (); + g_signal_connect (mm->monitor, "low-memory-warning", G_CALLBACK (low_memory_warning_cb), mm); +#endif /* HAS_MEMORY_MONITOR */ + + xdp_dbus_memory_monitor_set_version (XDP_DBUS_MEMORY_MONITOR (mm), 1); +} + +static void +memory_monitor_finalize (GObject *object) +{ +#ifdef HAS_MEMORY_MONITOR + MemoryMonitor *mm = (MemoryMonitor *) object; + + g_clear_object (&mm->monitor); +#endif /* HAS_MEMORY_MONITOR */ + + G_OBJECT_CLASS (memory_monitor_parent_class)->finalize (object); +} + +static void +memory_monitor_class_init (MemoryMonitorClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = memory_monitor_finalize; +} + +GDBusInterfaceSkeleton * +memory_monitor_create (GDBusConnection *connection) +{ + memory_monitor = g_object_new (memory_monitor_get_type (), NULL); + + return G_DBUS_INTERFACE_SKELETON (memory_monitor); +} diff --git a/src/memory-monitor.h b/src/memory-monitor.h new file mode 100644 index 0000000..c8e742a --- /dev/null +++ b/src/memory-monitor.h @@ -0,0 +1,25 @@ +/* + * Copyright © 2016, 2019 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +#pragma once + +#include + +GDBusInterfaceSkeleton * memory_monitor_create (GDBusConnection *connection); diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 0000000..44d2d51 --- /dev/null +++ b/src/meson.build @@ -0,0 +1,189 @@ +###### xdg-desktop-portal + +portal_built_sources = gnome.gdbus_codegen( + 'xdp-dbus', + sources: portal_sources + flatpak_intf_sources, + interface_prefix: 'org.freedesktop.portal', + namespace: 'XdpDbus', + docbook: 'portal', + autocleanup: 'none', +) +impl_built_sources = gnome.gdbus_codegen( + 'xdp-impl-dbus', + sources: portal_impl_sources, + interface_prefix: 'org.freedesktop.impl.portal', + namespace: 'XdpDbusImpl', + docbook: 'portal', + autocleanup: 'none', +) +background_monitor_sources = gnome.gdbus_codegen( + 'xdp-background-dbus', + sources: background_monitor_sources, + interface_prefix: 'org.freedesktop.background', + namespace: 'XdpDbusBackground', + docbook: 'portal', + autocleanup: 'all', +) +if have_geoclue + geoclue_built_sources = gnome.gdbus_codegen( + 'geoclue-dbus', + sources: 'org.freedesktop.GeoClue2.Client.xml', + interface_prefix: 'org.freedesktop.GeoClue2', + namespace: 'Geoclue', + autocleanup: 'none', + ) +else + geoclue_built_sources = [] +endif + +built_resources = gnome.compile_resources( + 'xdg-desktop-resources', + 'xdg-desktop-portal.gresource.xml', + c_name: '_xdg_desktop', + source_dir: ['..'], +) + +sd_escape_sources = files('sd-escape.c') + +xdg_desktop_portal_sources = files( + 'account.c', + 'background.c', + 'background-monitor.c', + 'call.c', + 'camera.c', + 'clipboard.c', + 'device.c', + 'documents.c', + 'dynamic-launcher.c', + 'email.c', + 'file-chooser.c', + 'flatpak-instance.c', + 'gamemode.c', + 'glib-backports.c', + 'global-shortcuts.c', + 'inhibit.c', + 'input-capture.c', + 'memory-monitor.c', + 'network-monitor.c', + 'notification.c', + 'open-uri.c', + 'permissions.c', + 'pipewire.c', + 'portal-impl.c', + 'power-profile-monitor.c', + 'print.c', + 'proxy-resolver.c', + 'realtime.c', + 'remote-desktop.c', + 'request.c', + 'restore-token.c', + 'screen-cast.c', + 'screenshot.c', + 'secret.c', + 'session.c', + 'settings.c', + 'trash.c', + 'wallpaper.c', + 'xdg-desktop-portal.c', + 'xdp-utils.c', +) + +xdg_desktop_portal_sources += [ + portal_built_sources, + impl_built_sources, + background_monitor_sources, + geoclue_built_sources, + built_resources, +] + +if have_geoclue + xdg_desktop_portal_sources += files( + 'location.c', + ) +endif + +if have_libsystemd + xdg_desktop_portal_sources += sd_escape_sources +endif + +common_deps = [ + glib_dep, + gio_dep, + gio_unix_dep, + json_glib_dep, +] + +xdg_desktop_portal_deps = common_deps + [ + geoclue_dep, + pipewire_dep, + libsystemd_dep, +] + +incs_xdg_desktop_portal = [ + include_directories('../document-portal'), + src_includes, + common_includes, +] + +xdg_desktop_portal = executable( + 'xdg-desktop-portal', + xdg_desktop_portal_sources, + dependencies: xdg_desktop_portal_deps, + include_directories: incs_xdg_desktop_portal, + install: true, + install_dir: libexecdir, +) + +xdp_utils_includes = include_directories('.') +xdp_utils_sources = files('xdp-utils.c') + +configure_file( + input: 'xdg-desktop-portal.service.in', + output: '@BASENAME@', + configuration: base_config, + install: true, + install_dir: systemd_userunit_dir, +) + +configure_file( + input: 'org.freedesktop.portal.Desktop.service.in', + output: '@BASENAME@', + configuration: base_config, + install: true, + install_dir: dbus_service_dir, +) + +helper_def = '' +if bwrap.found() + helper_def = '-DHELPER="@0@"'.format(bwrap.full_path()) +endif + +executable( + 'xdg-desktop-portal-validate-icon', + 'validate-icon.c', + dependencies: [gdk_pixbuf_dep], + c_args: [ + '-D_GNU_SOURCE=1', + helper_def + ], + install: true, + install_dir: libexecdir, +) + +configure_file( + input: 'xdg-desktop-portal-rewrite-launchers.service.in', + output: '@BASENAME@', + configuration: base_config, + install: true, + install_dir: systemd_userunit_dir, +) + +executable( + 'xdg-desktop-portal-rewrite-launchers', + 'rewrite-launchers.c', + 'glib-backports.c', + dependencies: common_deps, + include_directories: common_includes, + install: true, + install_dir: libexecdir, +) diff --git a/src/network-monitor.c b/src/network-monitor.c new file mode 100644 index 0000000..6e5b526 --- /dev/null +++ b/src/network-monitor.c @@ -0,0 +1,244 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +#include "config.h" + +#include +#include + +#include "network-monitor.h" +#include "request.h" +#include "xdp-dbus.h" +#include "xdp-utils.h" + +typedef struct _NetworkMonitor NetworkMonitor; +typedef struct _NetworkMonitorClass NetworkMonitorClass; + +struct _NetworkMonitor +{ + XdpDbusNetworkMonitorSkeleton parent_instance; + + GNetworkMonitor *monitor; +}; + +struct _NetworkMonitorClass +{ + XdpDbusNetworkMonitorSkeletonClass parent_class; +}; + +static NetworkMonitor *network_monitor; + +GType network_monitor_get_type (void) G_GNUC_CONST; +static void network_monitor_iface_init (XdpDbusNetworkMonitorIface *iface); + +G_DEFINE_TYPE_WITH_CODE (NetworkMonitor, network_monitor, + XDP_DBUS_TYPE_NETWORK_MONITOR_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_NETWORK_MONITOR, + network_monitor_iface_init)); + +static gboolean +handle_get_available (XdpDbusNetworkMonitor *object, + GDBusMethodInvocation *invocation) +{ + Request *request = request_from_invocation (invocation); + + if (!xdp_app_info_has_network (request->app_info)) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "This call is not available inside the sandbox"); + } + else + { + NetworkMonitor *nm = (NetworkMonitor *)object; + gboolean available = g_network_monitor_get_network_available (nm->monitor); + + g_dbus_method_invocation_return_value (invocation, g_variant_new ("(b)", available)); + } + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_get_metered (XdpDbusNetworkMonitor *object, + GDBusMethodInvocation *invocation) +{ + Request *request = request_from_invocation (invocation); + + if (!xdp_app_info_has_network (request->app_info)) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "This call is not available inside the sandbox"); + } + else + { + NetworkMonitor *nm = (NetworkMonitor *)object; + gboolean metered = g_network_monitor_get_network_metered (nm->monitor); + + g_dbus_method_invocation_return_value (invocation, g_variant_new ("(b)", metered)); + } + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_get_connectivity (XdpDbusNetworkMonitor *object, + GDBusMethodInvocation *invocation) +{ + Request *request = request_from_invocation (invocation); + + if (!xdp_app_info_has_network (request->app_info)) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "This call is not available inside the sandbox"); + } + else + { + NetworkMonitor *nm = (NetworkMonitor *)object; + GNetworkConnectivity connectivity = g_network_monitor_get_connectivity (nm->monitor); + + g_dbus_method_invocation_return_value (invocation, g_variant_new ("(u)", connectivity)); + } + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_get_status (XdpDbusNetworkMonitor *object, + GDBusMethodInvocation *invocation) +{ + Request *request = request_from_invocation (invocation); + + if (!xdp_app_info_has_network (request->app_info)) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "This call is not available inside the sandbox"); + } + else + { + NetworkMonitor *nm = (NetworkMonitor *)object; + GVariantBuilder status; + gboolean b; + guint c; + + g_variant_builder_init (&status, G_VARIANT_TYPE_VARDICT); + b = g_network_monitor_get_network_available (nm->monitor); + g_variant_builder_add (&status, "{sv}", + "available", g_variant_new_boolean (b)); + b = g_network_monitor_get_network_metered (nm->monitor); + g_variant_builder_add (&status, "{sv}", + "metered", g_variant_new_boolean (b)); + c = g_network_monitor_get_connectivity (nm->monitor); + g_variant_builder_add (&status, "{sv}", + "connectivity", g_variant_new_uint32 (c)); + g_dbus_method_invocation_return_value (invocation, g_variant_new ("(a{sv})", &status)); + } + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +can_reach_done (GObject *source, + GAsyncResult *result, + gpointer data) +{ + GNetworkMonitor *monitor = G_NETWORK_MONITOR (source); + g_autoptr(GDBusMethodInvocation) invocation = data; + gboolean reachable; + + reachable = g_network_monitor_can_reach_finish (monitor, result, NULL); + + g_dbus_method_invocation_return_value (invocation, g_variant_new ("(b)", reachable)); +} + +static gboolean +handle_can_reach (XdpDbusNetworkMonitor *object, + GDBusMethodInvocation *invocation, + const char *hostname, + guint port) +{ + Request *request = request_from_invocation (invocation); + + if (!xdp_app_info_has_network (request->app_info)) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "This call is not available inside the sandbox"); + } + else + { + NetworkMonitor *nm = (NetworkMonitor *)object; + g_autoptr(GSocketConnectable) address = NULL; + + address = g_network_address_new (hostname, port); + g_network_monitor_can_reach_async (nm->monitor, address, NULL, can_reach_done, g_object_ref (invocation)); + } + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +network_monitor_iface_init (XdpDbusNetworkMonitorIface *iface) +{ + iface->handle_get_available = handle_get_available; + iface->handle_get_metered = handle_get_metered; + iface->handle_get_connectivity = handle_get_connectivity; + iface->handle_get_status = handle_get_status; + iface->handle_can_reach = handle_can_reach; +} + +static void +network_changed (GObject *object, + gboolean network_available, + NetworkMonitor *nm) +{ + xdp_dbus_network_monitor_emit_changed (XDP_DBUS_NETWORK_MONITOR (nm)); +} + +static void +network_monitor_init (NetworkMonitor *nm) +{ + nm->monitor = g_network_monitor_get_default (); + + g_signal_connect (nm->monitor, "network-changed", G_CALLBACK (network_changed), nm); + + xdp_dbus_network_monitor_set_version (XDP_DBUS_NETWORK_MONITOR (nm), 3); +} + +static void +network_monitor_class_init (NetworkMonitorClass *klass) +{ +} + +GDBusInterfaceSkeleton * +network_monitor_create (GDBusConnection *connection) +{ + network_monitor = g_object_new (network_monitor_get_type (), NULL); + + return G_DBUS_INTERFACE_SKELETON (network_monitor); +} diff --git a/src/network-monitor.h b/src/network-monitor.h new file mode 100644 index 0000000..551f487 --- /dev/null +++ b/src/network-monitor.h @@ -0,0 +1,25 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +#pragma once + +#include + +GDBusInterfaceSkeleton * network_monitor_create (GDBusConnection *connection); diff --git a/src/notification.c b/src/notification.c new file mode 100644 index 0000000..b42a1be --- /dev/null +++ b/src/notification.c @@ -0,0 +1,600 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +#include "config.h" + +#include +#include +#include +#include + +#include "notification.h" +#include "request.h" +#include "permissions.h" +#include "xdp-dbus.h" +#include "xdp-utils.h" + +#define PERMISSION_TABLE "notifications" +#define PERMISSION_ID "notification" + +typedef struct _Notification Notification; +typedef struct _NotificationClass NotificationClass; + +struct _Notification +{ + XdpDbusNotificationSkeleton parent_instance; +}; + +struct _NotificationClass +{ + XdpDbusNotificationSkeletonClass parent_class; +}; + +static XdpDbusImplNotification *impl; +static Notification *notification; +G_LOCK_DEFINE (active); +static GHashTable *active; + +typedef struct { + char *app_id; + char *id; +} Pair; + +static guint +pair_hash (gconstpointer v) +{ + const Pair *p = v; + + return g_str_hash (p->app_id) + g_str_hash (p->id); +} + +static gboolean +pair_equal (gconstpointer v1, + gconstpointer v2) +{ + const Pair *p1 = v1; + const Pair *p2 = v2; + + return g_str_equal (p1->app_id, p2->app_id) && g_str_equal (p1->id, p2->id); +} + +static void +pair_free (gpointer v) +{ + Pair *p = v; + + g_free (p->app_id); + g_free (p->id); + g_free (p); +} + +static Pair * +pair_copy (Pair *o) +{ + Pair *p; + + p = g_new (Pair, 1); + p->app_id = g_strdup (o->app_id); + p->id = g_strdup (o->id); + + return p; +} + +GType notification_get_type (void) G_GNUC_CONST; +static void notification_iface_init (XdpDbusNotificationIface *iface); + +G_DEFINE_TYPE_WITH_CODE (Notification, notification, + XDP_DBUS_TYPE_NOTIFICATION_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_NOTIFICATION, + notification_iface_init)); + +static void +add_done (GObject *source, + GAsyncResult *result, + gpointer data) +{ + g_autoptr(Request) request = data; + g_autoptr(GError) error = NULL; + + if (!xdp_dbus_impl_notification_call_add_notification_finish (impl, result, &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("Backend call failed: %s", error->message); + } + else + { + Pair p; + + p.app_id = (char *)xdp_app_info_get_id (request->app_info); + p.id = (char *)g_object_get_data (G_OBJECT (request), "id"); + + G_LOCK (active); + g_hash_table_insert (active, pair_copy (&p), g_strdup (request->sender)); + G_UNLOCK (active); + } +} + +static gboolean +get_notification_allowed (const char *app_id) +{ + Permission permission; + + permission = get_permission_sync (app_id, PERMISSION_TABLE, PERMISSION_ID); + + if (permission == PERMISSION_NO) + return FALSE; + + if (permission == PERMISSION_UNSET) + { + g_debug ("No notification permissions stored for %s: allowing", app_id); + + set_permission_sync (app_id, PERMISSION_TABLE, PERMISSION_ID, PERMISSION_YES); + } + + return TRUE; +} + +static gboolean +check_value_type (const char *key, + GVariant *value, + const GVariantType *type, + GError **error) +{ + if (g_variant_is_of_type (value, type)) + return TRUE; + + g_set_error (error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "expected type for key %s is %s, found %s", + key, (const char *)type, (const char *)g_variant_get_type (value)); + + return FALSE; +} + +static gboolean +check_priority (GVariant *value, + GError **error) +{ + const char *priorities[] = { "low", "normal", "high", "urgent", NULL }; + + if (!check_value_type ("priority", value, G_VARIANT_TYPE_STRING, error)) + return FALSE; + + if (!g_strv_contains (priorities, g_variant_get_string (value, NULL))) + { + g_set_error (error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "%s not a priority", g_variant_get_string (value, NULL)); + return FALSE; + } + + return TRUE; +} + +static gboolean +check_button (GVariant *button, + GError **error) +{ + int i; + gboolean has_label = FALSE; + gboolean has_action = FALSE; + + for (i = 0; i < g_variant_n_children (button); i++) + { + const char *key; + g_autoptr(GVariant) value = NULL; + + g_variant_get_child (button, i, "{&sv}", &key, &value); + if (strcmp (key, "label") == 0) + has_label = TRUE; + else if (strcmp (key, "action") == 0) + has_action = TRUE; + else if (strcmp (key, "target") == 0) + ; + else + { + g_set_error (error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "%s not valid key", key); + return FALSE; + } + } + + if (!has_label) + { + g_set_error_literal (error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "label key is missing"); + return FALSE; + } + + if (!has_action) + { + g_set_error_literal (error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "action key is missing"); + return FALSE; + } + + return TRUE; +} + +static gboolean +check_buttons (GVariant *value, + GError **error) +{ + int i; + + if (!check_value_type ("buttons", value, G_VARIANT_TYPE ("aa{sv}"), error)) + return FALSE; + + for (i = 0; i < g_variant_n_children (value); i++) + { + g_autoptr(GVariant) button = g_variant_get_child_value (value, i); + + if (!check_button (button, error)) + { + g_prefix_error (error, "invalid button: "); + return FALSE; + } + } + return TRUE; +} + +static gboolean +check_serialized_icon (GVariant *value, + GError **error) +{ + g_autoptr(GIcon) icon = g_icon_deserialize (value); + + if (!icon) + { + g_set_error_literal (error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "invalid icon"); + return FALSE; + } + + if (!G_IS_BYTES_ICON (icon) && !G_IS_THEMED_ICON (icon)) + { + g_set_error_literal (error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "invalid icon"); + return FALSE; + } + + return TRUE; +} + +static gboolean +check_notification (GVariant *notification, + GError **error) +{ + int i; + + if (!check_value_type ("notification", notification, G_VARIANT_TYPE_VARDICT, error)) + return FALSE; + + for (i = 0; i < g_variant_n_children (notification); i++) + { + const char *key; + g_autoptr(GVariant) value = NULL; + + g_variant_get_child (notification, i, "{&sv}", &key, &value); + if (strcmp (key, "title") == 0 || + strcmp (key, "body") == 0) + { + if (!check_value_type (key, value, G_VARIANT_TYPE_STRING, error)) + return FALSE; + } + else if (strcmp (key, "icon") == 0) + { + if (!check_serialized_icon (value, error)) + return FALSE; + } + else if (strcmp (key, "priority") == 0) + { + if (!check_priority (value, error)) + return FALSE; + } + else if (strcmp (key, "default-action") == 0) + { + if (!check_value_type (key, value, G_VARIANT_TYPE_STRING, error)) + return FALSE; + } + else if (strcmp (key, "default-action-target") == 0) + ; + else if (strcmp (key, "buttons") == 0) + { + if (!check_buttons (value, error)) + return FALSE; + } + else + { + g_set_error (error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "%s not valid key", key); + return FALSE; + } + } + + return TRUE; +} + +static GVariant * +maybe_remove_icon (GVariant *notification) +{ + GVariantBuilder n; + int i; + + g_variant_builder_init (&n, G_VARIANT_TYPE_VARDICT); + for (i = 0; i < g_variant_n_children (notification); i++) + { + const char *key; + g_autoptr(GVariant) value = NULL; + + g_variant_get_child (notification, i, "{&sv}", &key, &value); + if (strcmp (key, "icon") != 0 || xdp_validate_serialized_icon (value, FALSE, NULL, NULL)) + g_variant_builder_add (&n, "{sv}", key, value); + } + + return g_variant_ref_sink (g_variant_builder_end (&n)); +} + +static void +handle_add_in_thread_func (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + Request *request = (Request *)task_data; + const char *id; + GVariant *notification; + g_autoptr(GVariant) notification2 = NULL; + + REQUEST_AUTOLOCK (request); + + if (!xdp_app_info_is_host (request->app_info) && + !get_notification_allowed (xdp_app_info_get_id (request->app_info))) + return; + + id = (const char *)g_object_get_data (G_OBJECT (request), "id"); + notification = (GVariant *)g_object_get_data (G_OBJECT (request), "notification"); + + notification2 = maybe_remove_icon (notification); + xdp_dbus_impl_notification_call_add_notification (impl, + xdp_app_info_get_id (request->app_info), + id, + notification2, + NULL, + add_done, + g_object_ref (request)); +} + +static gboolean +notification_handle_add_notification (XdpDbusNotification *object, + GDBusMethodInvocation *invocation, + const char *arg_id, + GVariant *notification) +{ + Request *request = request_from_invocation (invocation); + g_autoptr(GTask) task = NULL; + g_autoptr(GError) error = NULL; + + g_object_set_data_full (G_OBJECT (request), "id", g_strdup (arg_id), g_free); + g_object_set_data_full (G_OBJECT (request), "notification", g_variant_ref (notification), (GDestroyNotify)g_variant_unref); + + if (!check_notification (notification, &error)) + { + g_prefix_error (&error, "invalid notification: "); + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + task = g_task_new (object, NULL, NULL, NULL); + g_task_set_task_data (task, g_object_ref (request), g_object_unref); + g_task_run_in_thread (task, handle_add_in_thread_func); + + xdp_dbus_notification_complete_add_notification (object, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +remove_done (GObject *source, + GAsyncResult *result, + gpointer data) +{ + g_autoptr(Request) request = data; + g_autoptr(GError) error = NULL; + + if (!xdp_dbus_impl_notification_call_remove_notification_finish (impl, result, &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("Backend call failed: %s", error->message); + } + else + { + Pair p; + + p.app_id = (char *)xdp_app_info_get_id (request->app_info); + p.id = (char *)g_object_get_data (G_OBJECT (request), "id"); + + G_LOCK (active); + g_hash_table_remove (active, &p); + G_UNLOCK (active); + } +} + +static gboolean +notification_handle_remove_notification (XdpDbusNotification *object, + GDBusMethodInvocation *invocation, + const char *arg_id) +{ + Request *request = request_from_invocation (invocation); + + g_object_set_data_full (G_OBJECT (request), "id", g_strdup (arg_id), g_free); + + xdp_dbus_impl_notification_call_remove_notification (impl, + xdp_app_info_get_id (request->app_info), + arg_id, + NULL, + remove_done, g_object_ref (request)); + + xdp_dbus_notification_complete_remove_notification (object, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +action_invoked (GDBusConnection *connection, + const gchar *sender_name, + const gchar *object_path, + const gchar *interface_name, + const gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + Pair p; + const char *action; + GVariant *param; + const char *sender; + + g_variant_get (parameters, "(&s&s&s@av)", &p.app_id, &p.id, &action, ¶m); + + sender = g_hash_table_lookup (active, &p); + if (sender == NULL) + return; + + g_dbus_connection_emit_signal (connection, + sender, + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.Notification", + "ActionInvoked", + g_variant_new ("(ss@av)", + p.id, action, + param), + NULL); + +} + +static void +name_owner_changed (GDBusConnection *connection, + const gchar *sender_name, + const gchar *object_path, + const gchar *interface_name, + const gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + const char *name, *from, *to; + + g_variant_get (parameters, "(sss)", &name, &from, &to); + + if (name[0] == ':' && + strcmp (name, from) == 0 && + strcmp (to, "") == 0) + { + GHashTableIter iter; + Pair *p; + + G_LOCK (active); + + g_hash_table_iter_init (&iter, active); + while (g_hash_table_iter_next (&iter, (gpointer *)&p, NULL)) + { + if (g_strcmp0 (p->app_id, name) == 0) + g_hash_table_iter_remove (&iter); + } + + G_UNLOCK (active); + } +} + +static void +notification_iface_init (XdpDbusNotificationIface *iface) +{ + iface->handle_add_notification = notification_handle_add_notification; + iface->handle_remove_notification = notification_handle_remove_notification; +} + +static void +notification_init (Notification *notification) +{ + xdp_dbus_notification_set_version (XDP_DBUS_NOTIFICATION (notification), 1); +} + +static void +notification_class_init (NotificationClass *klass) +{ +} + +GDBusInterfaceSkeleton * +notification_create (GDBusConnection *connection, + const char *dbus_name) +{ + g_autoptr(GError) error = NULL; + + impl = xdp_dbus_impl_notification_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_NONE, + dbus_name, + DESKTOP_PORTAL_OBJECT_PATH, + NULL, &error); + if (impl == NULL) + { + g_warning ("Failed to create notification proxy: %s", error->message); + return NULL; + } + + g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (impl), G_MAXINT); + + notification = g_object_new (notification_get_type (), NULL); + active = g_hash_table_new_full (pair_hash, pair_equal, pair_free, g_free); + + g_dbus_connection_signal_subscribe (connection, + dbus_name, + "org.freedesktop.impl.portal.Notification", + "ActionInvoked", + DESKTOP_PORTAL_OBJECT_PATH, + NULL, + G_DBUS_SIGNAL_FLAGS_NONE, + action_invoked, + NULL, NULL); + + g_dbus_connection_signal_subscribe (connection, + "org.freedesktop.DBus", + "org.freedesktop.DBus", + "NameOwnerChanged", + "/org/freedesktop/DBus", + NULL, + G_DBUS_SIGNAL_FLAGS_NONE, + name_owner_changed, + NULL, NULL); + + return G_DBUS_INTERFACE_SKELETON (notification); +} diff --git a/src/notification.h b/src/notification.h new file mode 100644 index 0000000..7a918f9 --- /dev/null +++ b/src/notification.h @@ -0,0 +1,27 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + + +#pragma once + +#include + +GDBusInterfaceSkeleton * notification_create (GDBusConnection *connection, + const char *dbus_name); diff --git a/src/open-uri.c b/src/open-uri.c new file mode 100644 index 0000000..ef9cf49 --- /dev/null +++ b/src/open-uri.c @@ -0,0 +1,1133 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +#include "config.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include "open-uri.h" +#include "request.h" +#include "xdp-dbus.h" +#include "xdp-impl-dbus.h" +#include "xdp-utils.h" +#include "permissions.h" +#include "documents.h" + +#define FILE_MANAGER_DBUS_NAME "org.freedesktop.FileManager1" +#define FILE_MANAGER_DBUS_IFACE "org.freedesktop.FileManager1" +#define FILE_MANAGER_DBUS_PATH "/org/freedesktop/FileManager1" + +#define FILE_MANAGER_SHOW_ITEMS "ShowItems" + +#define PERMISSION_TABLE "desktop-used-apps" + +#define DEFAULT_THRESHOLD 3 + +typedef struct _OpenURI OpenURI; + +typedef struct _OpenURIClass OpenURIClass; + +struct _OpenURI +{ + XdpDbusOpenURISkeleton parent_instance; +}; + +struct _OpenURIClass +{ + XdpDbusOpenURISkeletonClass parent_class; +}; + +enum { + PERM_APP_ID, + PERM_APP_COUNT, + PERM_APP_THRESHOLD, + LAST_PERM +}; + +static XdpDbusImplAppChooser *impl; +static OpenURI *open_uri; +static GAppInfoMonitor *monitor; +static XdpDbusImplLockdown *lockdown; + +GType open_uri_get_type (void) G_GNUC_CONST; +static void open_uri_iface_init (XdpDbusOpenURIIface *iface); + +G_DEFINE_TYPE_WITH_CODE (OpenURI, open_uri, XDP_DBUS_TYPE_OPEN_URI_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_OPEN_URI, + open_uri_iface_init)); + +static void +parse_permissions (const char **permissions, + char **app_id, + gint *app_count, + gint *app_threshold) +{ + char *perms_id = NULL; + gint perms_count = 0; + gint perms_threshold = DEFAULT_THRESHOLD; + + if ((permissions != NULL) && + (permissions[PERM_APP_ID] != NULL) && + (permissions[PERM_APP_COUNT] != NULL)) + { + perms_id = g_strdup (permissions[PERM_APP_ID]); + perms_count = atoi (permissions[PERM_APP_COUNT]); + if (permissions[PERM_APP_THRESHOLD] != NULL) + { + g_autofree char *threshold = g_strdup (permissions[PERM_APP_THRESHOLD]); + if (g_strstrip(threshold)[0] != '\0') + perms_threshold = atoi (permissions[PERM_APP_THRESHOLD]); + } + } + + *app_id = perms_id; + *app_count = perms_count; + *app_threshold = perms_threshold; +} + +static gboolean +get_latest_choice_info (const char *app_id, + const char *content_type, + gchar **latest_id, + gint *latest_count, + gint *latest_threshold, + gboolean *always_ask) +{ + char *choice_id = NULL; + int choice_count = 0; + int choice_threshold = DEFAULT_THRESHOLD; + gboolean ask = FALSE; + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) out_perms = NULL; + g_autoptr(GVariant) out_data = NULL; + + if (!xdp_dbus_impl_permission_store_call_lookup_sync (get_permission_store (), + PERMISSION_TABLE, + content_type, + &out_perms, + &out_data, + NULL, + &error)) + { + g_dbus_error_strip_remote_error (error); + /* Not finding an entry for the content type in the permission store is perfectly ok */ + if (!g_error_matches (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_FOUND)) + g_warning ("Unable to retrieve info for '%s' in the %s table of the permission store: %s", + content_type, PERMISSION_TABLE, error->message); + g_clear_error (&error); + } + + if (out_data != NULL) + { + g_autoptr(GVariant) data = g_variant_get_child_value (out_data, 0); + if (g_variant_is_of_type (data, G_VARIANT_TYPE_VARDICT)) + g_variant_lookup (data, "always-ask", "b", &ask); + } + + if (out_perms != NULL) + { + GVariantIter iter; + GVariant *child; + gboolean app_found = FALSE; + + g_variant_iter_init (&iter, out_perms); + while (!app_found && (child = g_variant_iter_next_value (&iter))) + { + const char *child_app_id; + g_autofree const char **permissions; + + g_variant_get (child, "{&s^a&s}", &child_app_id, &permissions); + if (g_strcmp0 (child_app_id, app_id) == 0) + { + parse_permissions (permissions, &choice_id, &choice_count, &choice_threshold); + app_found = TRUE; + } + g_variant_unref (child); + } + } + + *latest_id = choice_id; + *latest_count = choice_count; + *latest_threshold = choice_threshold; + *always_ask = ask; + + g_debug ("Found in permission store: handler: %s, count: %d / %d, always ask: %d", + choice_id, choice_count, choice_threshold, ask); + + return (choice_id != NULL); +} + +static gboolean +is_sandboxed (GDesktopAppInfo *info) +{ + g_autofree char *flatpak = NULL; + + flatpak = g_desktop_app_info_get_string (G_DESKTOP_APP_INFO (info), "X-Flatpak"); + + return flatpak != NULL; +} + +/* This returns the desktop file basename without extension. + * We cant' just use the flatpak ID, since flatpaks are allowed + * to export 'sub ids', like the org.libreoffice.LibreOffice + * flatpak exporting org.libreoffice.LibreOffice.Impress.desktop, + * and we need to track the actual handlers. + * + * We still strip the .desktop extension, since that is what + * the backends expect. + */ +static char * +get_app_id (GAppInfo *info) +{ + const char *desktop_id; + + desktop_id = g_app_info_get_id (info); + + return g_strndup (desktop_id, strlen (desktop_id) - strlen (".desktop")); +} + +static gboolean +is_file_uri (const char *uri) +{ + g_autofree char *scheme = NULL; + + scheme = g_uri_parse_scheme (uri); + + if (g_strcmp0 (scheme, "file") == 0) + return TRUE; + + return FALSE; +} + +static gboolean +launch_application_with_uri (const char *choice_id, + const char *uri, + const char *parent_window, + gboolean writable, + const char *activation_token, + GError **error) +{ + g_autofree char *desktop_id = g_strconcat (choice_id, ".desktop", NULL); + g_autoptr(GDesktopAppInfo) info = g_desktop_app_info_new (desktop_id); + g_autoptr(GAppLaunchContext) context = g_app_launch_context_new (); + g_autofree char *ruri = NULL; + DocumentFlags flags = DOCUMENT_FLAG_NONE; + GList uris; + + if (info == NULL) + { + g_debug ("Cannot launch %s because desktop file does not exist", desktop_id); + g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_FOUND, "Desktop file %s does not exist", desktop_id); + return FALSE; + } + + g_debug ("Launching %s %s", choice_id, uri); + + if (is_sandboxed (info) && is_file_uri (uri)) + { + g_autoptr(GError) local_error = NULL; + + g_debug ("Registering %s for %s", uri, choice_id); + if (writable) + flags |= DOCUMENT_FLAG_WRITABLE; + + ruri = register_document (uri, choice_id, flags, &local_error); + if (ruri == NULL) + { + g_warning ("Error registering %s for %s: %s", uri, choice_id, local_error->message); + g_propagate_error (error, local_error); + return FALSE; + } + } + else + ruri = g_strdup (uri); + + g_app_launch_context_setenv (context, "PARENT_WINDOW_ID", parent_window); + + if (activation_token) + g_app_launch_context_setenv (context, "XDG_ACTIVATION_TOKEN", activation_token); + + uris.data = (gpointer)ruri; + uris.next = NULL; + + g_app_info_launch_uris (G_APP_INFO (info), &uris, context, error); + + return TRUE; +} + +static void +update_permissions_store (const char *app_id, + const char *content_type, + const char *chosen_id) +{ + g_autoptr(GError) error = NULL; + g_autofree char *latest_id = NULL; + gint latest_count; + gint latest_threshold; + g_auto(GStrv) in_permissions = NULL; + gboolean ask; + + if (get_latest_choice_info (app_id, content_type, + &latest_id, &latest_count, &latest_threshold, &ask) && + (g_strcmp0 (chosen_id, latest_id) == 0)) + { + /* same app chosen once again: update the counter */ + if (latest_count >= latest_threshold) + latest_count = latest_threshold; + else + latest_count++; + } + else + { + latest_id = g_strdup (chosen_id); + latest_count = 1; + } + + in_permissions = (GStrv) g_new0 (char *, LAST_PERM + 1); + in_permissions[PERM_APP_ID] = g_strdup (latest_id); + in_permissions[PERM_APP_COUNT] = g_strdup_printf ("%u", latest_count); + in_permissions[PERM_APP_THRESHOLD] = g_strdup_printf ("%u", latest_threshold); + + g_debug ("updating permissions for %s: content-type %s, handler %s, count %s / %s", + app_id, + content_type, + in_permissions[PERM_APP_ID], + in_permissions[PERM_APP_COUNT], + in_permissions[PERM_APP_THRESHOLD]); + + + if (!xdp_dbus_impl_permission_store_call_set_permission_sync (get_permission_store (), + PERMISSION_TABLE, + TRUE, + content_type, + app_id, + (const char * const*) in_permissions, + NULL, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("Error updating permission store: %s", error->message); + g_clear_error (&error); + } +} + +static void +send_response_in_thread_func (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + Request *request = (Request *)task_data; + guint response; + GVariant *options; + const char *choice; + GVariantBuilder opt_builder; + + REQUEST_AUTOLOCK (request); + + response = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (request), "response")); + options = (GVariant *)g_object_get_data (G_OBJECT (request), "options"); + + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + + if (response != 0) + goto out; + + if (g_variant_lookup (options, "choice", "&s", &choice)) + { + const char *uri; + const char *parent_window; + gboolean writable; + const char *content_type; + const char *activation_token = NULL; + + g_debug ("Received choice %s", choice); + + uri = (const char *)g_object_get_data (G_OBJECT (request), "uri"); + parent_window = (const char *)g_object_get_data (G_OBJECT (request), "parent-window"); + writable = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (request), "writable")); + content_type = (const char *)g_object_get_data (G_OBJECT (request), "content-type"); + + g_variant_lookup (options, "activation_token", "&s", &activation_token); + + if (launch_application_with_uri (choice, uri, parent_window, writable, activation_token, NULL)) + update_permissions_store (xdp_app_info_get_id (request->app_info), content_type, choice); + } + +out: + if (request->exported) + { + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + response, + g_variant_builder_end (&opt_builder)); + request_unexport (request); + } +} + +static void +app_chooser_done (GObject *source, + GAsyncResult *result, + gpointer data) +{ + g_autoptr (Request) request = data; + guint response = 2; + g_autoptr(GVariant) options = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GTask) task = NULL; + + if (!xdp_dbus_impl_app_chooser_call_choose_application_finish (XDP_DBUS_IMPL_APP_CHOOSER (source), + &response, + &options, + result, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("Backend call failed: %s", error->message); + } + + g_object_set_data (G_OBJECT (request), "response", GINT_TO_POINTER (response)); + if (options) + g_object_set_data_full (G_OBJECT (request), "options", g_variant_ref (options), (GDestroyNotify)g_variant_unref); + + task = g_task_new (NULL, NULL, NULL, NULL); + g_task_set_task_data (task, g_object_ref (request), g_object_unref); + g_task_run_in_thread (task, send_response_in_thread_func); +} + +static void +resolve_scheme_and_content_type (const char *uri, + char **scheme, + char **content_type) +{ + g_autofree char *uri_scheme = NULL; + + uri_scheme = g_uri_parse_scheme (uri); + if (uri_scheme && uri_scheme[0] != '\0') + *scheme = g_ascii_strdown (uri_scheme, -1); + + if (*scheme == NULL) + return; + + if (strcmp (*scheme, "file") == 0) + { + g_debug ("Not handling file uri %s", uri); + return; + } + + *content_type = g_strconcat ("x-scheme-handler/", *scheme, NULL); + g_debug ("Content type for %s uri %s: %s", uri, *scheme, *content_type); +} + +static void +get_content_type_for_file (const char *path, + char **content_type) +{ + g_autoptr(GError) error = NULL; + g_autoptr(GFile) file = g_file_new_for_path (path); + g_autoptr(GFileInfo) info = g_file_query_info (file, + G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, + 0, + NULL, + &error); + + if (info != NULL) + { + *content_type = g_strdup (g_file_info_get_content_type (info)); + g_debug ("Content type for file %s: %s", path, *content_type); + } + else + { + g_debug ("Failed to fetch content type for file %s: %s", path, error->message); + } +} + +static gboolean +should_use_default_app (const char *scheme, + const char *content_type) +{ + const char *skipped_schemes[] = { + "http", + "https", + "ftp", + "mailto", + "webcal", + "calendar", + NULL + }; + + /* We skip the app chooser for Internet URIs, to be open in the browser, + * mail client, or calendar, as well as for directories to be opened in + * the file manager */ + if (g_strv_contains (skipped_schemes, scheme) || + g_strcmp0 (content_type, "inode/directory") == 0) + { + g_debug ("Can skip app chooser for %s", content_type); + return TRUE; + } + + return FALSE; +} + +static void +find_recommended_choices (const char *scheme, + const char *content_type, + char **default_app, + GStrv *choices, + guint *choices_len) +{ + GAppInfo *info; + GList *infos, *l; + guint n_choices = 0; + GStrv result = NULL; + int i; + + info = g_app_info_get_default_for_type (content_type, FALSE); + + if (info != NULL) + { + *default_app = get_app_id (info); + g_debug ("Default handler %s for %s, %s", *default_app, scheme, content_type); + } + else + { + *default_app = NULL; + g_debug ("No default handler for %s, %s", scheme, content_type); + } + + infos = g_app_info_get_recommended_for_type (content_type); + /* Use fallbacks if we have no recommended application for this type */ + if (!infos) + infos = g_app_info_get_all_for_type (content_type); + + n_choices = g_list_length (infos); + result = g_new (char *, n_choices + 1); + for (l = infos, i = 0; l; l = l->next) + { + info = l->data; + result[i++] = get_app_id (info); + } + result[i] = NULL; + g_list_free_full (infos, g_object_unref); + + { + g_autofree char *a = g_strjoinv (", ", result); + g_debug ("Recommended handlers for %s, %s: %s", scheme, content_type, a); + } + + *choices = result; + *choices_len = n_choices; +} + +static void +app_info_changed (GAppInfoMonitor *monitor, + Request *request) +{ + const char *scheme; + const char *content_type; + g_autofree char *default_app = NULL; + g_auto(GStrv) choices = NULL; + guint n_choices; + + scheme = (const char *)g_object_get_data (G_OBJECT (request), "scheme"); + content_type = (const char *)g_object_get_data (G_OBJECT (request), "content-type"); + find_recommended_choices (scheme, content_type, &default_app, &choices, &n_choices); + + xdp_dbus_impl_app_chooser_call_update_choices (impl, + request->id, + (const char * const *)choices, + NULL, + NULL, + NULL); +} + +static gboolean +app_exists (const char *app_id) +{ + g_autoptr(GDesktopAppInfo) info = NULL; + g_autofree gchar *with_desktop = NULL; + + g_return_val_if_fail (app_id != NULL, FALSE); + + with_desktop = g_strconcat (app_id, ".desktop", NULL); + info = g_desktop_app_info_new (with_desktop); + return (info != NULL); +} + +static void +handle_open_in_thread_func (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + Request *request = (Request *)task_data; + const char *parent_window; + const char *app_id = xdp_app_info_get_id (request->app_info); + const char *activation_token; + g_autofree char *uri = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + g_autofree char *default_app = NULL; + g_auto(GStrv) choices = NULL; + guint n_choices; + g_autofree char *scheme = NULL; + g_autofree char *content_type = NULL; + g_autofree char *latest_id = NULL; + g_autofree char *basename = NULL; + gint latest_count; + gint latest_threshold; + gboolean ask_for_content_type; + GVariantBuilder opts_builder; + gboolean skip_app_chooser = FALSE; + g_auto(XdpFd) fd = -1; + gboolean writable = FALSE; + gboolean ask = FALSE; + gboolean open_dir = FALSE; + gboolean use_default_app = FALSE; + const char *reason; + + parent_window = (const char *)g_object_get_data (G_OBJECT (request), "parent-window"); + uri = g_strdup ((const char *)g_object_get_data (G_OBJECT (request), "uri")); + fd = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (request), "fd")); + writable = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (request), "writable")); + ask = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (request), "ask")); + open_dir = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (request), "open-dir")); + activation_token = (const char *)g_object_get_data (G_OBJECT (request), "activation-token"); + + REQUEST_AUTOLOCK (request); + + /* Verify that either uri or fd is set, not both */ + if (uri != NULL && fd != -1) + { + g_warning ("Rejecting invalid open-uri request (both URI and fd are set)"); + if (request->exported) + { + g_variant_builder_init (&opts_builder, G_VARIANT_TYPE_VARDICT); + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + XDG_DESKTOP_PORTAL_RESPONSE_OTHER, + g_variant_builder_end (&opts_builder)); + request_unexport (request); + } + return; + } + + if (uri) + { + GError *error = NULL; + + if (!g_uri_is_valid (uri, G_URI_FLAGS_NONE, &error)) + { + g_debug ("Rejecting open request for invalid uri '%s': %s", uri, error->message); + g_clear_error (&error); + + /* Reject the request */ + if (request->exported) + { + g_variant_builder_init (&opts_builder, G_VARIANT_TYPE_VARDICT); + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + XDG_DESKTOP_PORTAL_RESPONSE_OTHER, + g_variant_builder_end (&opts_builder)); + request_unexport (request); + } + return; + } + + resolve_scheme_and_content_type (uri, &scheme, &content_type); + if (content_type == NULL) + { + /* Reject the request */ + if (request->exported) + { + g_debug ("Rejecting open request as content-type couldn't be fetched for '%s'", uri); + g_variant_builder_init (&opts_builder, G_VARIANT_TYPE_VARDICT); + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + XDG_DESKTOP_PORTAL_RESPONSE_OTHER, + g_variant_builder_end (&opts_builder)); + request_unexport (request); + } + return; + } + } + else + { + g_autofree char *path = NULL; + gboolean fd_is_writable; + g_autoptr(GError) local_error = NULL; + g_autofree char *host_path = NULL; + + path = xdp_app_info_get_path_for_fd (request->app_info, fd, 0, NULL, &fd_is_writable, &local_error); + + host_path = get_real_path_for_doc_path (path, request->app_info); + if (host_path) + { + g_debug ("OpenFile: translating path value '%s' to host path '%s'", path, host_path); + g_clear_pointer (&path, g_free); + path = g_steal_pointer (&host_path); + } + + if (path == NULL || + (writable && !fd_is_writable) || + (!xdp_app_info_is_host (request->app_info) && !writable && fd_is_writable)) + { + /* Reject the request */ + if (path == NULL) + { + g_debug ("Rejecting open request: %s", local_error->message); + } + else + { + g_debug ("Rejecting open request for %s as opening %swritable but fd is %swritable", + path, writable ? "" : "not ", fd_is_writable ? "" : "not "); + } + + if (request->exported) + { + g_variant_builder_init (&opts_builder, G_VARIANT_TYPE_VARDICT); + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + XDG_DESKTOP_PORTAL_RESPONSE_OTHER, + g_variant_builder_end (&opts_builder)); + request_unexport (request); + } + return; + } + + if (open_dir) + { + g_autofree char *real_path = get_real_path_for_doc_path (path, request->app_info); + /* Try opening the directory via the file manager interface, then + fall back to a plain URI open */ + g_autoptr(GError) local_error = NULL; + g_autoptr(GVariant) result = NULL; + g_autoptr(GVariantBuilder) uris_builder = NULL; + g_autofree char* item_uri = g_filename_to_uri (real_path, NULL, NULL); + g_autoptr(GDBusConnection) bus = NULL; + + bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &local_error); + + uris_builder = g_variant_builder_new (G_VARIANT_TYPE ("as")); + g_variant_builder_add (uris_builder, "s", item_uri); + + if (bus) + result = g_dbus_connection_call_sync (bus, + FILE_MANAGER_DBUS_NAME, + FILE_MANAGER_DBUS_PATH, + FILE_MANAGER_DBUS_IFACE, + FILE_MANAGER_SHOW_ITEMS, + g_variant_new ("(ass)", uris_builder, ""), + NULL, /* ignore returned type */ + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + &local_error); + + if (result == NULL) + { + if (g_error_matches (local_error, G_DBUS_ERROR, + G_DBUS_ERROR_NAME_HAS_NO_OWNER) || + g_error_matches (local_error, G_DBUS_ERROR, + G_DBUS_ERROR_SERVICE_UNKNOWN)) + g_debug ("No " FILE_MANAGER_DBUS_NAME " available"); + else + g_warning ("Failed to call " FILE_MANAGER_SHOW_ITEMS ": %s", + local_error->message); + } + else + { + g_variant_builder_init (&opts_builder, G_VARIANT_TYPE_VARDICT); + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + XDG_DESKTOP_PORTAL_RESPONSE_SUCCESS, + g_variant_builder_end (&opts_builder)); + request_unexport (request); + return; + } + + g_free (path); + path = g_path_get_dirname (real_path); + } + + get_content_type_for_file (path, &content_type); + basename = g_path_get_basename (path); + + scheme = g_strdup ("file"); + uri = g_filename_to_uri (path, NULL, NULL); + g_object_set_data_full (G_OBJECT (request), "uri", g_strdup (uri), g_free); + close (fd); + fd = -1; + g_object_set_data (G_OBJECT (request), "fd", GINT_TO_POINTER (-1)); + } + + g_object_set_data_full (G_OBJECT (request), "scheme", g_strdup (scheme), g_free); + g_object_set_data_full (G_OBJECT (request), "content-type", g_strdup (content_type), g_free); + + /* collect all the information */ + find_recommended_choices (scheme, content_type, &default_app, &choices, &n_choices); + /* it's never NULL, but might be empty (only contain the NULL terminator) */ + g_assert (choices != NULL); + if (default_app != NULL && !app_exists (default_app)) + g_clear_pointer (&default_app, g_free); + use_default_app = should_use_default_app (scheme, content_type); + get_latest_choice_info (app_id, content_type, + &latest_id, &latest_count, &latest_threshold, + &ask_for_content_type); + if (latest_id != NULL && !app_exists (latest_id)) + g_clear_pointer (&latest_id, g_free); + + skip_app_chooser = FALSE; + reason = NULL; + + /* apply default handling: skip if the we have a default handler */ + if (default_app != NULL && use_default_app) + { + reason = "Allowing to skip app chooser: can use default"; + skip_app_chooser = TRUE; + } + + if (n_choices == 1) + { + if (!skip_app_chooser) + reason = "Allowing to skip app chooser: no choice"; + skip_app_chooser = TRUE; + } + + /* also skip if the user has made the same choice often enough */ + if (latest_id != NULL && latest_count >= latest_threshold) + { + if (!skip_app_chooser) + reason = "Allowing to skip app chooser: above threshold"; + skip_app_chooser = TRUE; + } + + /* respect the app choices */ + if (ask) + { + if (skip_app_chooser) + reason = "Refusing to skip app chooser: app request"; + skip_app_chooser = FALSE; + } + + /* respect the users choices: paranoid mode overrides everything else */ + if (ask_for_content_type || latest_threshold == G_MAXINT) + { + if (skip_app_chooser) + reason = "Refusing to skip app chooser: always-ask enabled"; + skip_app_chooser = FALSE; + } + + g_debug ("%s", reason); + + if (skip_app_chooser) + { + const char *app = NULL; + + if (default_app != NULL && use_default_app) + app = default_app; + else if (latest_id != NULL) + app = latest_id; + else if (default_app != NULL) + app = default_app; + else if (n_choices > 0 && app_exists (choices[0])) + app = choices[0]; + + if (app) + { + /* Launch the app directly */ + g_autoptr(GError) error = NULL; + + g_debug ("Skipping app chooser"); + + gboolean result = launch_application_with_uri (app, uri, parent_window, writable, activation_token, &error); + if (request->exported) + { + if (!result) + g_debug ("Open request for '%s' failed: %s", uri, error->message); + g_variant_builder_init (&opts_builder, G_VARIANT_TYPE_VARDICT); + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + result ? XDG_DESKTOP_PORTAL_RESPONSE_SUCCESS : XDG_DESKTOP_PORTAL_RESPONSE_OTHER, + g_variant_builder_end (&opts_builder)); + request_unexport (request); + } + + return; + } + } + + g_variant_builder_init (&opts_builder, G_VARIANT_TYPE_VARDICT); + + if (latest_id != NULL) + g_variant_builder_add (&opts_builder, "{sv}", "last_choice", g_variant_new_string (latest_id)); + else if (default_app != NULL) + g_variant_builder_add (&opts_builder, "{sv}", "last_choice", g_variant_new_string (default_app)); + + g_object_set_data_full (G_OBJECT (request), "content-type", g_strdup (content_type), g_free); + + g_variant_builder_add (&opts_builder, "{sv}", "content_type", g_variant_new_string (content_type)); + if (basename) + g_variant_builder_add (&opts_builder, "{sv}", "filename", g_variant_new_string (basename)); + if (uri) + g_variant_builder_add (&opts_builder, "{sv}", "uri", g_variant_new_string (uri)); + if (activation_token) + g_variant_builder_add (&opts_builder, "{sv}", "activation_token", g_variant_new_string (uri)); + + impl_request = + xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, NULL); + + request_set_impl_request (request, impl_request); + + g_signal_connect_object (monitor, "changed", G_CALLBACK (app_info_changed), request, 0); + + g_debug ("Opening app chooser"); + + xdp_dbus_impl_app_chooser_call_choose_application (impl, + request->id, + app_id, + parent_window, + (const char * const *)choices, + g_variant_builder_end (&opts_builder), + NULL, + app_chooser_done, + g_object_ref (request)); +} + +static gboolean +handle_open_uri (XdpDbusOpenURI *object, + GDBusMethodInvocation *invocation, + const gchar *arg_parent_window, + const gchar *arg_uri, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + g_autoptr(GTask) task = NULL; + gboolean writable; + gboolean ask; + const char *activation_token = NULL; + + if (xdp_dbus_impl_lockdown_get_disable_application_handlers (lockdown)) + { + g_debug ("Application handlers disabled"); + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Application handlers disabled"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + if (!g_variant_lookup (arg_options, "writable", "b", &writable)) + writable = FALSE; + + if (!g_variant_lookup (arg_options, "ask", "b", &ask)) + ask = FALSE; + + g_variant_lookup (arg_options, "activation_token", "&s", &activation_token); + + g_object_set_data (G_OBJECT (request), "fd", GINT_TO_POINTER (-1)); + g_object_set_data_full (G_OBJECT (request), "uri", g_strdup (arg_uri), g_free); + g_object_set_data_full (G_OBJECT (request), "parent-window", g_strdup (arg_parent_window), g_free); + g_object_set_data (G_OBJECT (request), "writable", GINT_TO_POINTER (writable)); + g_object_set_data (G_OBJECT (request), "ask", GINT_TO_POINTER (ask)); + + if (activation_token) + g_object_set_data_full (G_OBJECT (request), "activation-token", g_strdup (activation_token), g_free); + + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + xdp_dbus_open_uri_complete_open_uri (object, invocation, request->id); + + task = g_task_new (object, NULL, NULL, NULL); + g_task_set_task_data (task, g_object_ref (request), g_object_unref); + g_task_run_in_thread (task, handle_open_in_thread_func); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_open_file (XdpDbusOpenURI *object, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + const gchar *arg_parent_window, + GVariant *arg_fd, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + g_autoptr(GTask) task = NULL; + gboolean writable; + gboolean ask; + int fd_id, fd; + const char *activation_token = NULL; + g_autoptr(GError) error = NULL; + + if (xdp_dbus_impl_lockdown_get_disable_application_handlers (lockdown)) + { + g_debug ("Application handlers disabled"); + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Application handlers disabled"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + if (!g_variant_lookup (arg_options, "writable", "b", &writable)) + writable = FALSE; + + if (!g_variant_lookup (arg_options, "ask", "b", &ask)) + ask = FALSE; + + g_variant_get (arg_fd, "h", &fd_id); + fd = g_unix_fd_list_get (fd_list, fd_id, &error); + if (fd == -1) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_lookup (arg_options, "activation_token", "&s", &activation_token); + + g_object_set_data (G_OBJECT (request), "fd", GINT_TO_POINTER (fd)); + g_object_set_data_full (G_OBJECT (request), "parent-window", g_strdup (arg_parent_window), g_free); + g_object_set_data (G_OBJECT (request), "writable", GINT_TO_POINTER (writable)); + g_object_set_data (G_OBJECT (request), "ask", GINT_TO_POINTER (ask)); + + if (activation_token) + g_object_set_data_full (G_OBJECT (request), "activation-token", g_strdup (activation_token), g_free); + + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + xdp_dbus_open_uri_complete_open_file (object, invocation, NULL, request->id); + + task = g_task_new (object, NULL, NULL, NULL); + g_task_set_task_data (task, g_object_ref (request), g_object_unref); + g_task_run_in_thread (task, handle_open_in_thread_func); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_open_directory (XdpDbusOpenURI *object, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + const gchar *arg_parent_window, + GVariant *arg_fd, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + g_autoptr(GTask) task = NULL; + int fd_id, fd; + const char *activation_token = NULL; + g_autoptr(GError) error = NULL; + + if (xdp_dbus_impl_lockdown_get_disable_application_handlers (lockdown)) + { + g_debug ("Application handlers disabled"); + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Application handlers disabled"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_get (arg_fd, "h", &fd_id); + fd = g_unix_fd_list_get (fd_list, fd_id, &error); + if (fd == -1) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_lookup (arg_options, "activation_token", "&s", &activation_token); + + g_object_set_data (G_OBJECT (request), "fd", GINT_TO_POINTER (fd)); + g_object_set_data_full (G_OBJECT (request), "parent-window", g_strdup (arg_parent_window), g_free); + g_object_set_data (G_OBJECT (request), "writable", GINT_TO_POINTER (0)); + g_object_set_data (G_OBJECT (request), "ask", GINT_TO_POINTER (0)); + g_object_set_data (G_OBJECT (request), "open-dir", GINT_TO_POINTER (1)); + + if (activation_token) + g_object_set_data_full (G_OBJECT (request), "activation-token", g_strdup (activation_token), g_free); + + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + xdp_dbus_open_uri_complete_open_file (object, invocation, NULL, request->id); + + task = g_task_new (object, NULL, NULL, NULL); + g_task_set_task_data (task, g_object_ref (request), g_object_unref); + g_task_run_in_thread (task, handle_open_in_thread_func); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +open_uri_iface_init (XdpDbusOpenURIIface *iface) +{ + iface->handle_open_uri = handle_open_uri; + iface->handle_open_file = handle_open_file; + iface->handle_open_directory = handle_open_directory; +} + +static void +open_uri_init (OpenURI *openuri) +{ + xdp_dbus_open_uri_set_version (XDP_DBUS_OPEN_URI (openuri), 3); +} + +static void +open_uri_class_init (OpenURIClass *klass) +{ +} + +GDBusInterfaceSkeleton * +open_uri_create (GDBusConnection *connection, + const char *dbus_name, + gpointer lockdown_proxy) +{ + g_autoptr(GError) error = NULL; + + lockdown = lockdown_proxy; + + impl = xdp_dbus_impl_app_chooser_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_NONE, + dbus_name, + DESKTOP_PORTAL_OBJECT_PATH, + NULL, &error); + if (impl == NULL) + { + g_warning ("Failed to create app chooser proxy: %s", error->message); + return NULL; + } + + g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (impl), G_MAXINT); + + open_uri = g_object_new (open_uri_get_type (), NULL); + + monitor = g_app_info_monitor_get (); + + return G_DBUS_INTERFACE_SKELETON (open_uri); +} + diff --git a/src/open-uri.h b/src/open-uri.h new file mode 100644 index 0000000..531252a --- /dev/null +++ b/src/open-uri.h @@ -0,0 +1,28 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + + +#pragma once + +#include + +GDBusInterfaceSkeleton * open_uri_create (GDBusConnection *connection, + const char *dbus_name, + gpointer lockdown); diff --git a/src/org.freedesktop.GeoClue2.Client.xml b/src/org.freedesktop.GeoClue2.Client.xml new file mode 100644 index 0000000..c539170 --- /dev/null +++ b/src/org.freedesktop.GeoClue2.Client.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/org.freedesktop.portal.Desktop.service.in b/src/org.freedesktop.portal.Desktop.service.in new file mode 100644 index 0000000..2b31f32 --- /dev/null +++ b/src/org.freedesktop.portal.Desktop.service.in @@ -0,0 +1,5 @@ +[D-BUS Service] +Name=org.freedesktop.portal.Desktop +Exec=@libexecdir@/xdg-desktop-portal +SystemdService=xdg-desktop-portal.service +AssumedAppArmorLabel=unconfined diff --git a/src/permissions.c b/src/permissions.c new file mode 100644 index 0000000..ff09c87 --- /dev/null +++ b/src/permissions.c @@ -0,0 +1,182 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +#include "config.h" + +#include + +#include "permissions.h" + +static XdpDbusImplPermissionStore *permission_store = NULL; + +char ** +get_permissions_sync (const char *app_id, + const char *table, + const char *id) +{ + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) out_perms = NULL; + g_autoptr(GVariant) out_data = NULL; + g_autofree char **permissions = NULL; + + if (!xdp_dbus_impl_permission_store_call_lookup_sync (permission_store, + table, + id, + &out_perms, + &out_data, + NULL, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_debug ("No '%s' permissions found: %s", table, error->message); + return NULL; + } + + if (!g_variant_lookup (out_perms, app_id, "^a&s", &permissions)) + { + g_debug ("No permissions stored for: %s %s, app %s", table, id, app_id); + + return NULL; + } + + return g_strdupv (permissions); +} + +Permission +permissions_to_tristate (char **permissions) +{ + if (g_strv_length ((char **)permissions) != 1) + { + g_autofree char *a = g_strjoinv (" ", (char **)permissions); + g_warning ("Wrong permission format, ignoring (%s)", a); + return PERMISSION_UNSET; + } + + if (strcmp (permissions[0], "yes") == 0) + return PERMISSION_YES; + else if (strcmp (permissions[0], "no") == 0) + return PERMISSION_NO; + else if (strcmp (permissions[0], "ask") == 0) + return PERMISSION_ASK; + else + { + g_autofree char *a = g_strjoinv (" ", (char **)permissions); + g_warning ("Wrong permission format, ignoring (%s)", a); + } + + return PERMISSION_UNSET; +} + +char ** +permissions_from_tristate (Permission permission) +{ + char *permission_str; + char **permissions; + + switch (permission) + { + case PERMISSION_UNSET: + return NULL; + case PERMISSION_NO: + permission_str = g_strdup ("no"); + break; + case PERMISSION_YES: + permission_str = g_strdup ("yes"); + break; + case PERMISSION_ASK: + permission_str = g_strdup ("ask"); + break; + default: + g_assert_not_reached (); + break; + } + + permissions = g_new0 (char *, 2); + permissions[0] = permission_str; + + return permissions; +} + +void +set_permissions_sync (const char *app_id, + const char *table, + const char *id, + const char * const *permissions) +{ + g_autoptr(GError) error = NULL; + + if (!xdp_dbus_impl_permission_store_call_set_permission_sync (permission_store, + table, + TRUE, + id, + app_id, + permissions, + NULL, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("Error updating permission store: %s", error->message); + } +} + +Permission +get_permission_sync (const char *app_id, + const char *table, + const char *id) +{ + g_auto(GStrv) perms = NULL; + + perms = get_permissions_sync (app_id, table, id); + if (perms) + return permissions_to_tristate (perms); + + return PERMISSION_UNSET; +} + +void set_permission_sync (const char *app_id, + const char *table, + const char *id, + Permission permission) +{ + g_auto(GStrv) perms = NULL; + + perms = permissions_from_tristate (permission); + set_permissions_sync (app_id, table, id, (const char * const *)perms); +} + +void +init_permission_store (GDBusConnection *connection) +{ + g_autoptr(GError) error = NULL; + + permission_store = xdp_dbus_impl_permission_store_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_NONE, + "org.freedesktop.impl.portal.PermissionStore", + "/org/freedesktop/impl/portal/PermissionStore", + NULL, &error); + if (permission_store == NULL) + g_warning ("No permission store: %s", error->message); +} + +XdpDbusImplPermissionStore * +get_permission_store (void) +{ + return permission_store; +} diff --git a/src/permissions.h b/src/permissions.h new file mode 100644 index 0000000..528980a --- /dev/null +++ b/src/permissions.h @@ -0,0 +1,57 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +#pragma once + +#include +#include "xdp-impl-dbus.h" + +typedef enum _Permission +{ + PERMISSION_UNSET, + PERMISSION_NO, + PERMISSION_YES, + PERMISSION_ASK +} Permission; + +char **get_permissions_sync (const char *app_id, + const char *table, + const char *id); + +void set_permissions_sync (const char *app_id, + const char *table, + const char *id, + const char * const *permissions); + +Permission get_permission_sync (const char *app_id, + const char *table, + const char *id); + +void set_permission_sync (const char *app_id, + const char *table, + const char *id, + Permission permission); + +char **permissions_from_tristate (Permission permission); + +Permission permissions_to_tristate (char **permissions); + +void init_permission_store (GDBusConnection *connection); +XdpDbusImplPermissionStore *get_permission_store (void); diff --git a/src/pipewire.c b/src/pipewire.c new file mode 100644 index 0000000..4833a16 --- /dev/null +++ b/src/pipewire.c @@ -0,0 +1,356 @@ +/* + * Copyright © 2017-2019 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + */ + +#include "config.h" + +#include +#include +#include +#include + +#include "pipewire.h" + +#define ROUNDTRIP_TIMEOUT_SECS 10 + +typedef struct _PipeWireSource +{ + GSource base; + + PipeWireRemote *remote; +} PipeWireSource; + +static gboolean is_pipewire_initialized = FALSE; + +static void +registry_event_global (void *user_data, + uint32_t id, + uint32_t permissions, + const char *type, + uint32_t version, + const struct spa_dict *props) +{ + PipeWireRemote *remote = user_data; + const struct spa_dict_item *factory_object_type; + PipeWireGlobal *global; + + global = g_new0 (PipeWireGlobal, 1); + *global = (PipeWireGlobal) { + .parent_id = id, + }; + + g_hash_table_insert (remote->globals, GINT_TO_POINTER (id), global); + if (remote->global_added_cb) + remote->global_added_cb (remote, id, type, props, remote->user_data); + + if (strcmp(type, PW_TYPE_INTERFACE_Factory) != 0) + return; + + factory_object_type = spa_dict_lookup_item (props, "factory.type.name"); + if (!factory_object_type) + return; + + if (strcmp (factory_object_type->value, "PipeWire:Interface:ClientNode") == 0) + { + remote->node_factory_id = id; + pw_main_loop_quit (remote->loop); + } +} + +static void +registry_event_global_remove (void *user_data, + uint32_t id) +{ + PipeWireRemote *remote = user_data; + + if (remote->global_removed_cb) + remote->global_removed_cb (remote, id, remote->user_data); + g_hash_table_remove (remote->globals, GINT_TO_POINTER (id)); +} + +static const struct pw_registry_events registry_events = { + PW_VERSION_REGISTRY_EVENTS, + .global = registry_event_global, + .global_remove = registry_event_global_remove, +}; + +static void +on_roundtrip_timeout (void *user_data, + uint64_t expirations) +{ + PipeWireRemote *remote = user_data; + + g_warning ("PipeWire roundtrip timed out waiting for events"); + pw_main_loop_quit (remote->loop); +} + +void +pipewire_remote_roundtrip (PipeWireRemote *remote) +{ + struct timespec roundtrip_timeout_spec = { ROUNDTRIP_TIMEOUT_SECS, 0 }; + + remote->sync_seq = pw_core_sync (remote->core, PW_ID_CORE, remote->sync_seq); + + /* Arm the roundtrip timeout before running the main loop, then clear it + right afterwards. */ + pw_loop_update_timer (pw_main_loop_get_loop (remote->loop), + remote->roundtrip_timeout, + &roundtrip_timeout_spec, + NULL, + FALSE); + + pw_main_loop_run (remote->loop); + + pw_loop_update_timer (pw_main_loop_get_loop (remote->loop), + remote->roundtrip_timeout, + NULL, + NULL, + FALSE); +} + +static gboolean +discover_node_factory_sync (PipeWireRemote *remote, + GError **error) +{ + struct pw_registry *registry; + + registry = pw_core_get_registry (remote->core, PW_VERSION_REGISTRY, 0); + pw_registry_add_listener (registry, + &remote->registry_listener, + ®istry_events, + remote); + + pipewire_remote_roundtrip (remote); + + pw_proxy_destroy((struct pw_proxy*)registry); + + if (remote->node_factory_id == 0) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "No node factory discovered"); + return FALSE; + } + + return TRUE; +} + +static void +core_event_error (void *user_data, + uint32_t id, + int seq, + int res, + const char *message) +{ + PipeWireRemote *remote = user_data; + + if (id == PW_ID_CORE) + { + g_set_error (&remote->error, G_IO_ERROR, G_IO_ERROR_FAILED, + "%s", message); + pw_main_loop_quit (remote->loop); + } +} + +static void +core_event_done (void *user_data, + uint32_t id, int seq) +{ + PipeWireRemote *remote = user_data; + + if (id == PW_ID_CORE && remote->sync_seq == seq) + pw_main_loop_quit (remote->loop); +} + +static const struct pw_core_events core_events = { + PW_VERSION_CORE_EVENTS, + .error = core_event_error, + .done = core_event_done, +}; + +static gboolean +pipewire_loop_source_prepare (GSource *base, + int *timeout) +{ + *timeout = -1; + return FALSE; +} + +static gboolean +pipewire_loop_source_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + PipeWireSource *pipewire_source = (PipeWireSource *) source; + struct pw_loop *loop; + int result; + + loop = pw_main_loop_get_loop (pipewire_source->remote->loop); + result = pw_loop_iterate (loop, 0); + if (result < 0) + g_warning ("pipewire_loop_iterate failed: %s", spa_strerror (result)); + + if (pipewire_source->remote->error) + { + GFunc error_callback; + + g_warning ("Caught PipeWire error: %s", pipewire_source->remote->error->message); + + error_callback = pipewire_source->remote->error_callback; + if (error_callback) + error_callback (pipewire_source->remote, + pipewire_source->remote->user_data); + } + + return TRUE; +} + +static void +pipewire_loop_source_finalize (GSource *source) +{ + PipeWireSource *pipewire_source = (PipeWireSource *) source; + struct pw_loop *loop; + + loop = pw_main_loop_get_loop (pipewire_source->remote->loop); + pw_loop_leave (loop); +} + +static GSourceFuncs pipewire_source_funcs = +{ + pipewire_loop_source_prepare, + NULL, + pipewire_loop_source_dispatch, + pipewire_loop_source_finalize +}; + +void +pipewire_remote_destroy (PipeWireRemote *remote) +{ + if (remote->roundtrip_timeout != NULL) + { + struct pw_loop *loop = pw_main_loop_get_loop (remote->loop); + pw_loop_destroy_source (loop, g_steal_pointer (&remote->roundtrip_timeout)); + } + + g_clear_pointer (&remote->globals, g_hash_table_destroy); + g_clear_pointer (&remote->core, pw_core_disconnect); + g_clear_pointer (&remote->context, pw_context_destroy); + g_clear_pointer (&remote->loop, pw_main_loop_destroy); + g_clear_error (&remote->error); + + g_free (remote); +} + +static void +ensure_pipewire_is_initialized (void) +{ + if (is_pipewire_initialized) + return; + + pw_init (NULL, NULL); + + is_pipewire_initialized = TRUE; +} + +GSource * +pipewire_remote_create_source (PipeWireRemote *remote) +{ + PipeWireSource *pipewire_source; + struct pw_loop *loop; + + + pipewire_source = (PipeWireSource *) g_source_new (&pipewire_source_funcs, + sizeof (PipeWireSource)); + pipewire_source->remote = remote; + + loop = pw_main_loop_get_loop (pipewire_source->remote->loop); + g_source_add_unix_fd (&pipewire_source->base, + pw_loop_get_fd (loop), + G_IO_IN | G_IO_ERR); + + pw_loop_enter (loop); + g_source_attach (&pipewire_source->base, NULL); + + return &pipewire_source->base; +} + +PipeWireRemote * +pipewire_remote_new_sync (struct pw_properties *pipewire_properties, + PipeWireGlobalAddedCallback global_added_cb, + PipeWireGlobalRemovedCallback global_removed_cb, + GFunc error_callback, + gpointer user_data, + GError **error) +{ + PipeWireRemote *remote; + + ensure_pipewire_is_initialized (); + + remote = g_new0 (PipeWireRemote, 1); + + remote->global_added_cb = global_added_cb; + remote->global_removed_cb = global_removed_cb; + remote->error_callback = error_callback; + remote->user_data = user_data; + + remote->loop = pw_main_loop_new (NULL); + if (!remote->loop) + { + pipewire_remote_destroy (remote); + pw_properties_free (pipewire_properties); + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Couldn't create PipeWire main loop"); + return NULL; + } + + remote->context = pw_context_new (pw_main_loop_get_loop (remote->loop), NULL, 0); + if (!remote->context) + { + pipewire_remote_destroy (remote); + pw_properties_free (pipewire_properties); + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Couldn't create PipeWire context"); + return NULL; + } + + remote->core = pw_context_connect (remote->context, pipewire_properties, 0); + if (!remote->core) + { + pipewire_remote_destroy (remote); + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Couldn't connect to PipeWire"); + return NULL; + } + + remote->roundtrip_timeout = pw_loop_add_timer (pw_main_loop_get_loop (remote->loop), + on_roundtrip_timeout, + remote); + + remote->globals = g_hash_table_new_full (NULL, NULL, NULL, g_free); + + pw_core_add_listener (remote->core, + &remote->core_listener, + &core_events, + remote); + + if (!discover_node_factory_sync (remote, error)) + { + pipewire_remote_destroy (remote); + return NULL; + } + + return remote; +} diff --git a/src/pipewire.h b/src/pipewire.h new file mode 100644 index 0000000..f33d7af --- /dev/null +++ b/src/pipewire.h @@ -0,0 +1,78 @@ +/* + * Copyright © 2017-2018 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + */ + +#pragma once + +#include +#include +#include + +typedef struct _PipeWireRemote PipeWireRemote; + +typedef struct _PipeWireGlobal +{ + uint32_t parent_id; + gboolean permission_set; +} PipeWireGlobal; + +typedef void (* PipeWireGlobalAddedCallback) (PipeWireRemote *remote, + uint32_t id, + const char *type, + const struct spa_dict *props, + gpointer user_data); + +typedef void (* PipeWireGlobalRemovedCallback) (PipeWireRemote *remote, + uint32_t id, + gpointer user_data); + +struct _PipeWireRemote +{ + struct pw_main_loop *loop; + struct pw_context *context; + struct pw_core *core; + struct spa_hook core_listener; + + struct spa_source *roundtrip_timeout; + + int sync_seq; + + struct spa_hook registry_listener; + + GHashTable *globals; + PipeWireGlobalAddedCallback global_added_cb; + PipeWireGlobalRemovedCallback global_removed_cb; + gpointer user_data; + GFunc error_callback; + + uint32_t node_factory_id; + + GError *error; +}; + +PipeWireRemote * pipewire_remote_new_sync (struct pw_properties *pipewire_properties, + PipeWireGlobalAddedCallback global_added_cb, + PipeWireGlobalRemovedCallback global_removed_cb, + GFunc error_callback, + gpointer user_data, + GError **error); + +void pipewire_remote_destroy (PipeWireRemote *remote); + +void pipewire_remote_roundtrip (PipeWireRemote *remote); + +GSource * pipewire_remote_create_source (PipeWireRemote *remote); diff --git a/src/portal-impl.c b/src/portal-impl.c new file mode 100644 index 0000000..171bdbf --- /dev/null +++ b/src/portal-impl.c @@ -0,0 +1,571 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Alexander Larsson + * Matthias Clasen + */ + +#include "config.h" + +#include "portal-impl.h" + +#include +#include + +#include +#include + +typedef struct _PortalInterface { + /* dbus_name is NULL if this is the default */ + char *dbus_name; + char **portals; +} PortalInterface; + +typedef struct _PortalConfig { + char *source; + PortalInterface **ifaces; + size_t n_ifaces; + PortalInterface *dfl_portal; +} PortalConfig; + +static void +portal_interface_free (PortalInterface *iface) +{ + g_clear_pointer (&iface->dbus_name, g_free); + g_clear_pointer (&iface->portals, g_strfreev); + + g_free (iface); +} + +static void +portal_config_free (PortalConfig *config) +{ + g_clear_pointer (&config->source, g_free); + + for (size_t i = 0; i < config->n_ifaces; i++) + portal_interface_free (config->ifaces[i]); + + g_clear_pointer (&config->dfl_portal, portal_interface_free); + g_clear_pointer (&config->ifaces, g_free); + + g_free (config); +} + +static void +portal_implementation_free (PortalImplementation *impl) +{ + g_clear_pointer (&impl->source, g_free); + g_clear_pointer (&impl->dbus_name, g_free); + g_clear_pointer (&impl->interfaces, g_strfreev); + g_clear_pointer (&impl->use_in, g_strfreev); + g_free (impl); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(PortalImplementation, portal_implementation_free) +G_DEFINE_AUTOPTR_CLEANUP_FUNC(PortalInterface, portal_interface_free) +G_DEFINE_AUTOPTR_CLEANUP_FUNC(PortalConfig, portal_config_free) + +/* Validation code taken from gdesktopappinfo.c {{{ */ + +/* See: https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html + * + * There's not much to go on: a desktop name must be composed of alphanumeric + * characters, including '-' and '_'. Since we use this value to construct file + * names, we are going to need avoid invalid characters + */ +static gboolean +validate_xdg_desktop (const char *desktop) +{ + size_t i; + + for (i = 0; desktop[i] != '\0'; i++) + { + if (desktop[i] != '-' && + desktop[i] != '_' && + !g_ascii_isalnum (desktop[i])) + return FALSE; + } + + if (i == 0) + return FALSE; + + return TRUE; +} + +static char ** +get_valid_current_desktops (const char *value) +{ + char **tmp; + GPtrArray *valid_desktops; + + if (value == NULL) + value = g_getenv ("XDG_CURRENT_DESKTOP"); + if (value == NULL) + value = ""; + + tmp = g_strsplit (value, G_SEARCHPATH_SEPARATOR_S, 0); + valid_desktops = g_ptr_array_new_full (g_strv_length (tmp) + 1, g_free); + + for (size_t i = 0; tmp[i] != NULL; i++) + { + if (validate_xdg_desktop (tmp[i])) + g_ptr_array_add (valid_desktops, tmp[i]); + else + g_free (tmp[i]); + } + + g_ptr_array_add (valid_desktops, NULL); + g_free (tmp); + + tmp = (char **) g_ptr_array_steal (valid_desktops, NULL); + g_ptr_array_unref (valid_desktops); + + return tmp; +} + +static const char ** +get_current_lowercase_desktops (void) +{ + static char **result; + + if (g_once_init_enter (&result)) + { + char **tmp = get_valid_current_desktops (NULL); + + for (size_t i = 0; tmp[i] != NULL; i++) + { + /* Convert to lowercase */ + for (size_t j = 0; tmp[i][j] != '\0'; j++) + tmp[i][j] = g_ascii_tolower (tmp[i][j]); + } + + g_once_init_leave (&result, tmp); + } + + return (const char **) result; +} +/* }}} */ + +static PortalConfig *config = NULL; +static GList *implementations = NULL; + +static gboolean +register_portal (const char *path, + gboolean opt_verbose, + GError **error) +{ + g_autoptr(GKeyFile) keyfile = g_key_file_new (); + g_autoptr(PortalImplementation) impl = g_new0 (PortalImplementation, 1); + g_autofree char *basename = NULL; + int i; + + g_debug ("loading %s", path); + + if (!g_key_file_load_from_file (keyfile, path, G_KEY_FILE_NONE, error)) + return FALSE; + + basename = g_path_get_basename (path); + impl->source = g_strndup (basename, strrchr (basename, '.') - basename); + impl->dbus_name = g_key_file_get_string (keyfile, "portal", "DBusName", error); + if (impl->dbus_name == NULL) + return FALSE; + if (!g_dbus_is_name (impl->dbus_name)) + { + g_set_error (error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE, + "Not a valid bus name: %s", impl->dbus_name); + return FALSE; + } + + impl->interfaces = g_key_file_get_string_list (keyfile, "portal", "Interfaces", NULL, error); + if (impl->interfaces == NULL) + return FALSE; + for (i = 0; impl->interfaces[i]; i++) + { + if (!g_dbus_is_interface_name (impl->interfaces[i])) + { + g_set_error (error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE, + "Not a valid interface name: %s", impl->interfaces[i]); + return FALSE; + } + if (!g_str_has_prefix (impl->interfaces[i], "org.freedesktop.impl.portal.")) + { + g_set_error (error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE, + "Not a portal backend interface: %s", impl->interfaces[i]); + return FALSE; + } + } + + if (opt_verbose) + { + for (i = 0; impl->interfaces[i]; i++) + g_debug ("portal implementation supports %s", impl->interfaces[i]); + } + + impl->use_in = g_key_file_get_string_list (keyfile, "portal", "UseIn", NULL, error); + if (opt_verbose && impl->use_in != NULL) + { + g_autofree char *uses = g_strjoinv (", ", impl->use_in); + g_warning ("Portal %s uses the deprecated UseIn key; the preferred method to " + "match portal implementations to desktop environments is to use the " + "portals.conf configuration file", + uses); + } + + implementations = g_list_prepend (implementations, impl); + impl = NULL; + + return TRUE; +} + +static gboolean +g_strv_case_contains (const gchar * const *strv, + const gchar *str) +{ + for (; strv && *strv != NULL; strv++) + { + if (g_ascii_strcasecmp (str, *strv) == 0) + return TRUE; + } + + return FALSE; +} + +static gint +sort_impl_by_use_in_and_name (gconstpointer a, + gconstpointer b) +{ + const PortalImplementation *pa = a; + const PortalImplementation *pb = b; + const char **desktops; + int i; + + desktops = get_current_lowercase_desktops (); + + for (i = 0; desktops[i] != NULL; i++) + { + gboolean use_a = pa->use_in != NULL + ? g_strv_case_contains ((const char **)pa->use_in, desktops[i]) + : FALSE; + gboolean use_b = pb->use_in != NULL + ? g_strv_case_contains ((const char **)pb->use_in, desktops[i]) + : FALSE; + + if (use_a != use_b) + return use_b - use_a; + else if (use_a) + break; + else + continue; + } + + return strcmp (pa->source, pb->source); +} + +void +load_installed_portals (gboolean opt_verbose) +{ + const char *portal_dir; + g_autoptr(GFile) dir = NULL; + g_autoptr(GFileEnumerator) enumerator = NULL; + + /* We need to override this in the tests */ + portal_dir = g_getenv ("XDG_DESKTOP_PORTAL_DIR"); + if (portal_dir == NULL) + portal_dir = DATADIR "/xdg-desktop-portal/portals"; + + g_debug ("load portals from %s", portal_dir); + + dir = g_file_new_for_path (portal_dir); + enumerator = g_file_enumerate_children (dir, "*", G_FILE_QUERY_INFO_NONE, NULL, NULL); + + if (enumerator == NULL) + return; + + while (TRUE) + { + g_autoptr(GFileInfo) info = g_file_enumerator_next_file (enumerator, NULL, NULL); + g_autoptr(GFile) child = NULL; + g_autofree char *path = NULL; + const char *name; + g_autoptr(GError) error = NULL; + + if (info == NULL) + break; + + name = g_file_info_get_name (info); + + if (!g_str_has_suffix (name, ".portal")) + continue; + + child = g_file_enumerator_get_child (enumerator, info); + path = g_file_get_path (child); + + if (!register_portal (path, opt_verbose, &error)) + { + g_warning ("Error loading %s: %s", path, error->message); + continue; + } + } + + implementations = g_list_sort (implementations, sort_impl_by_use_in_and_name); +} + +static PortalConfig * +load_portal_configuration_for_dir (gboolean opt_verbose, + const char *base_directory, + const char *portal_file) +{ + g_autofree char *path = g_build_filename (base_directory, portal_file, NULL); + g_autoptr(GKeyFile) key_file = g_key_file_new (); + + g_debug ("Looking for portals configuration in '%s'", path); + if (!g_key_file_load_from_file (key_file, path, G_KEY_FILE_NONE, NULL)) + return NULL; + + g_auto(GStrv) ifaces = g_key_file_get_keys (key_file, "preferred", NULL, NULL); + + if (ifaces != NULL) + { + g_autoptr(GPtrArray) interfaces = g_ptr_array_new_full (g_strv_length (ifaces) + 1, NULL); + g_autoptr(PortalConfig) conf = g_new0 (PortalConfig, 1); + g_autoptr(PortalInterface) dfl_portal = NULL; + + for (size_t i = 0; ifaces[i] != NULL; i++) + { + g_autoptr(PortalInterface) interface = g_new0 (PortalInterface, 1); + + interface->dbus_name = g_strdup (ifaces[i]); + interface->portals = g_key_file_get_string_list (key_file, "preferred", ifaces[i], NULL, NULL); + if (interface->portals == NULL) + { + g_critical ("Invalid portals for interface '%s' in %s", ifaces[i], portal_file); + return NULL; + } + + if (opt_verbose) + { + g_autofree char *preferred = g_strjoinv (", ", interface->portals); + g_debug ("Preferred portals for interface '%s': %s", ifaces[i], preferred); + } + + if (strcmp (ifaces[i], "default") == 0) + dfl_portal = g_steal_pointer (&interface); + else + g_ptr_array_add (interfaces, g_steal_pointer (&interface)); + } + + conf->n_ifaces = interfaces->len; + conf->ifaces = (PortalInterface **) g_ptr_array_steal (interfaces, NULL); + conf->dfl_portal = g_steal_pointer (&dfl_portal); + + return g_steal_pointer (&conf); + } + + return NULL; +} + +void +load_portal_configuration (gboolean opt_verbose) +{ + g_autoptr(PortalConfig) conf = NULL; + g_autofree char *user_portal_dir = NULL; + const char **desktops; + const char *portal_dir; + + /* We need to override this in the tests */ + portal_dir = g_getenv ("XDG_DESKTOP_PORTAL_DIR"); + if (portal_dir == NULL) + portal_dir = SYSCONFDIR "/xdg-desktop-portal"; + + user_portal_dir = g_build_filename (g_get_user_config_dir (), + "xdg-desktop-portal", + NULL); + + conf = load_portal_configuration_for_dir (opt_verbose, user_portal_dir, "portals.conf"); + if (conf != NULL) + { + if (opt_verbose) + g_debug ("Using user portal configuration file"); + + config = g_steal_pointer (&conf); + } + + desktops = get_current_lowercase_desktops (); + for (size_t i = 0; desktops[i] != NULL; i++) + { + g_autofree char *portals_conf = g_strdup_printf ("%s-portals.conf", desktops[i]); + + conf = load_portal_configuration_for_dir (opt_verbose, user_portal_dir, portals_conf); + if (conf != NULL) + { + if (opt_verbose) + g_debug ("Using user portal configuration file '%s' for desktop '%s'", + portals_conf, + desktops[i]); + + config = g_steal_pointer (&conf); + return; + } + + conf = load_portal_configuration_for_dir (opt_verbose, portal_dir, portals_conf); + if (conf != NULL) + { + if (opt_verbose) + g_debug ("Using system portal configuration file '%s' for desktop '%s'", + portals_conf, + desktops[i]); + + config = g_steal_pointer (&conf); + return; + } + } +} + +static gboolean +portal_impl_name_matches (const PortalImplementation *impl, + const PortalInterface *iface) +{ + /* Exact match */ + if (g_strv_contains ((const char * const *) iface->portals, impl->source)) + { + g_debug ("Found '%s' in configuration for %s", impl->source, iface->dbus_name); + return TRUE; + } + + /* The "*" alias means "any" */ + if (g_strv_contains ((const char * const *) iface->portals, "*")) + { + g_debug ("Found '*' in configuration for %s", iface->dbus_name); + return TRUE; + } + + /* No portal */ + if (g_strv_contains ((const char * const *) iface->portals, "none")) + { + g_debug ("Found 'none' in configuration for %s", iface->dbus_name); + return FALSE; + } + + return FALSE; +} + +static gboolean +portal_impl_matches_config (const PortalImplementation *impl, + const char *interface) +{ + if (config == NULL) + return FALSE; + + /* Interfaces have precedence, followed by the "default" catch all, + * to allow for specific interfaces to override the default + */ + for (int i = 0; i < config->n_ifaces; i++) + { + const PortalInterface *iface = config->ifaces[i]; + + if (g_strcmp0 (iface->dbus_name, interface) == 0) + return portal_impl_name_matches (impl, iface); + } + + if (config->dfl_portal) + return portal_impl_name_matches (impl, config->dfl_portal); + + return FALSE; +} + +PortalImplementation * +find_portal_implementation (const char *interface) +{ + const char **desktops; + GList *l; + int i; + + desktops = get_current_lowercase_desktops (); + + for (i = 0; desktops[i] != NULL; i++) + { + for (l = implementations; l != NULL; l = l->next) + { + PortalImplementation *impl = l->data; + + if (!g_strv_contains ((const char **)impl->interfaces, interface)) + continue; + + if (portal_impl_matches_config (impl, interface)) + { + g_debug ("Using %s.portal for %s in %s (config)", impl->source, interface, desktops[i]); + return impl; + } + } + } + + /* Fallback to the old UseIn key */ + for (i = 0; desktops[i] != NULL; i++) + { + for (l = implementations; l != NULL; l = l->next) + { + PortalImplementation *impl = l->data; + + if (!g_strv_contains ((const char **)impl->interfaces, interface)) + continue; + + if (impl->use_in != NULL && g_strv_case_contains ((const char **)impl->use_in, desktops[i])) + { + g_debug ("Using %s.portal for %s in %s (fallback)", impl->source, interface, desktops[i]); + return impl; + } + } + } + +#if 0 + /* Fall back to *any* installed implementation */ + for (l = implementations; l != NULL; l = l->next) + { + PortalImplementation *impl = l->data; + + if (!g_strv_contains ((const char **)impl->interfaces, interface)) + continue; + + g_debug ("Falling back to %s.portal for %s", impl->source, interface); + return impl; + } +#endif + + return NULL; +} + +GPtrArray * +find_all_portal_implementations (const char *interface) +{ + GPtrArray *impls; + GList *l; + + impls = g_ptr_array_new (); + + for (l = implementations; l != NULL; l = l->next) + { + PortalImplementation *impl = l->data; + + if (g_strv_contains ((const char **)impl->interfaces, interface)) + { + g_debug ("Using %s.portal for %s", impl->source, interface); + g_ptr_array_add (impls, impl); + } + } + + return impls; +} diff --git a/src/portal-impl.h b/src/portal-impl.h new file mode 100644 index 0000000..4c82ac2 --- /dev/null +++ b/src/portal-impl.h @@ -0,0 +1,40 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Alexander Larsson + * Matthias Clasen + */ + +#ifndef __PORTAL_IMPL_H__ +#define __PORTAL_IMPL_H__ + +#include + +typedef struct { + char *source; + char *dbus_name; + char **interfaces; + char **use_in; + int priority; +} PortalImplementation; + +void load_installed_portals (gboolean opt_verbose); +void load_portal_configuration (gboolean opt_verbose); +PortalImplementation *find_portal_implementation (const char *interface); +GPtrArray *find_all_portal_implementations (const char *interface); + +#endif /* __PORTAL_IMPL_H__ */ diff --git a/src/power-profile-monitor.c b/src/power-profile-monitor.c new file mode 100644 index 0000000..26917f5 --- /dev/null +++ b/src/power-profile-monitor.c @@ -0,0 +1,115 @@ +/* + * Copyright © 2021 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Bastien Nocera + */ + +#include "config.h" + +#include +#include + +#include "power-profile-monitor.h" +#include "request.h" +#include "xdp-dbus.h" +#include "xdp-utils.h" + +#if GLIB_CHECK_VERSION(2, 69, 1) +#define HAS_POWER_PROFILE_MONITOR 1 +#endif + +typedef struct _PowerProfileMonitor PowerProfileMonitor; +typedef struct _PowerProfileMonitorClass PowerProfileMonitorClass; + +struct _PowerProfileMonitor +{ + XdpDbusPowerProfileMonitorSkeleton parent_instance; + +#ifdef HAS_POWER_PROFILE_MONITOR + GPowerProfileMonitor *monitor; +#endif /* HAS_POWER_PROFILE_MONITOR */ +}; + +struct _PowerProfileMonitorClass +{ + XdpDbusPowerProfileMonitorSkeletonClass parent_class; +}; + +static PowerProfileMonitor *power_profile_monitor; + +GType power_profile_monitor_get_type (void) G_GNUC_CONST; +static void power_profile_monitor_iface_init (XdpDbusPowerProfileMonitorIface *iface); + +G_DEFINE_TYPE_WITH_CODE (PowerProfileMonitor, power_profile_monitor, + XDP_DBUS_TYPE_POWER_PROFILE_MONITOR_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_POWER_PROFILE_MONITOR, + power_profile_monitor_iface_init)); + +static void +power_profile_monitor_iface_init (XdpDbusPowerProfileMonitorIface *iface) +{ +} + +#ifdef HAS_POWER_PROFILE_MONITOR +static void +power_saver_enabled_changed_cb (GObject *gobject, + GParamSpec *pspec, + PowerProfileMonitor *ppm) +{ + xdp_dbus_power_profile_monitor_set_power_saver_enabled (XDP_DBUS_POWER_PROFILE_MONITOR (ppm), + g_power_profile_monitor_get_power_saver_enabled (ppm->monitor)); +} +#endif /* HAS_POWER_PROFILE_MONITOR */ + +static void +power_profile_monitor_init (PowerProfileMonitor *ppm) +{ +#ifdef HAS_POWER_PROFILE_MONITOR + ppm->monitor = g_power_profile_monitor_dup_default (); + g_signal_connect (ppm->monitor, "notify::power-saver-enabled", G_CALLBACK (power_saver_enabled_changed_cb), ppm); +#endif /* HAS_POWER_PROFILE_MONITOR */ + + xdp_dbus_power_profile_monitor_set_version (XDP_DBUS_POWER_PROFILE_MONITOR (ppm), 1); +} + +static void +power_profile_monitor_finalize (GObject *object) +{ +#ifdef HAS_POWER_PROFILE_MONITOR + PowerProfileMonitor *ppm = (PowerProfileMonitor *) object; + + g_clear_object (&ppm->monitor); +#endif /* HAS_POWER_PROFILE_MONITOR */ + + G_OBJECT_CLASS (power_profile_monitor_parent_class)->finalize (object); +} + +static void +power_profile_monitor_class_init (PowerProfileMonitorClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = power_profile_monitor_finalize; +} + +GDBusInterfaceSkeleton * +power_profile_monitor_create (GDBusConnection *connection) +{ + power_profile_monitor = g_object_new (power_profile_monitor_get_type (), NULL); + + return G_DBUS_INTERFACE_SKELETON (power_profile_monitor); +} diff --git a/src/power-profile-monitor.h b/src/power-profile-monitor.h new file mode 100644 index 0000000..b23ea8b --- /dev/null +++ b/src/power-profile-monitor.h @@ -0,0 +1,25 @@ +/* + * Copyright © 2021 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Bastien Nocera + */ + +#pragma once + +#include + +GDBusInterfaceSkeleton * power_profile_monitor_create (GDBusConnection *connection); diff --git a/src/print.c b/src/print.c new file mode 100644 index 0000000..765e539 --- /dev/null +++ b/src/print.c @@ -0,0 +1,325 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +#include "config.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include "print.h" +#include "request.h" +#include "xdp-dbus.h" +#include "xdp-impl-dbus.h" +#include "xdp-utils.h" + +typedef struct _Print Print; +typedef struct _PrintClass PrintClass; + +struct _Print +{ + XdpDbusPrintSkeleton parent_instance; +}; + +struct _PrintClass +{ + XdpDbusPrintSkeletonClass parent_class; +}; + +static XdpDbusImplPrint *impl; +static Print *print; +static XdpDbusImplLockdown *lockdown; + +GType print_get_type (void) G_GNUC_CONST; +static void print_iface_init (XdpDbusPrintIface *iface); + +G_DEFINE_TYPE_WITH_CODE (Print, print, XDP_DBUS_TYPE_PRINT_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_PRINT, + print_iface_init)); + +static void +print_done (GObject *source, + GAsyncResult *result, + gpointer data) +{ + g_autoptr(Request) request = data; + guint response = 2; + g_autoptr(GVariant) options = NULL; + g_autoptr(GError) error = NULL; + + REQUEST_AUTOLOCK (request); + + if (!xdp_dbus_impl_print_call_print_finish (XDP_DBUS_IMPL_PRINT (source), + &response, + &options, + NULL, + result, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("Backend call failed: %s", error->message); + } + + if (request->exported) + { + GVariantBuilder opt_builder; + + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + response, + g_variant_builder_end (&opt_builder)); + request_unexport (request); + } +} + +static XdpOptionKey print_options[] = { + { "token", G_VARIANT_TYPE_UINT32, NULL }, + { "modal", G_VARIANT_TYPE_BOOLEAN, NULL }, +}; + +static gboolean +handle_print (XdpDbusPrint *object, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + const gchar *arg_parent_window, + const gchar *arg_title, + GVariant *arg_fd, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + const char *app_id = xdp_app_info_get_id (request->app_info); + g_autoptr(GError) error = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + GVariantBuilder opt_builder; + + if (xdp_dbus_impl_lockdown_get_disable_printing (lockdown)) + { + g_debug ("Printing disabled"); + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Printing disabled"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + + REQUEST_AUTOLOCK (request); + + impl_request = xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, &error); + if (!impl_request) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request (request, impl_request); + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + xdp_filter_options (arg_options, &opt_builder, + print_options, G_N_ELEMENTS (print_options), NULL); + xdp_dbus_impl_print_call_print(impl, + request->id, + app_id, + arg_parent_window, + arg_title, + arg_fd, + g_variant_builder_end (&opt_builder), + fd_list, + NULL, + print_done, + g_object_ref (request)); + + xdp_dbus_print_complete_print (object, invocation, NULL, request->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static XdpOptionKey response_options[] = { + { "settings", G_VARIANT_TYPE_VARDICT, NULL }, + { "page-setup", G_VARIANT_TYPE_VARDICT, NULL }, + { "token", G_VARIANT_TYPE_UINT32, NULL } +}; + +static void +prepare_print_done (GObject *source, + GAsyncResult *result, + gpointer data) +{ + g_autoptr(Request) request = data; + guint response = 2; + g_autoptr(GVariant) options = NULL; + g_autoptr(GError) error = NULL; + + REQUEST_AUTOLOCK (request); + + if (!xdp_dbus_impl_print_call_prepare_print_finish (XDP_DBUS_IMPL_PRINT (source), + &response, + &options, + result, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("Backend call failed: %s", error->message); + } + + if (request->exported) + { + GVariantBuilder opt_builder; + + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + + if (response == 0) + xdp_filter_options (options, &opt_builder, + response_options, G_N_ELEMENTS (response_options), + NULL); + + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + response, + g_variant_builder_end (&opt_builder)); + + request_unexport (request); + } +} + +static XdpOptionKey prepare_print_options[] = { + { "modal", G_VARIANT_TYPE_BOOLEAN }, + { "accept_label", G_VARIANT_TYPE_STRING } +}; + +static gboolean +handle_prepare_print (XdpDbusPrint *object, + GDBusMethodInvocation *invocation, + const gchar *arg_parent_window, + const gchar *arg_title, + GVariant *arg_settings, + GVariant *arg_page_setup, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + const char *app_id = xdp_app_info_get_id (request->app_info); + g_autoptr(GError) error = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + GVariantBuilder opt_builder; + + if (xdp_dbus_impl_lockdown_get_disable_printing (lockdown)) + { + g_debug ("Printing disabled"); + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Printing disabled"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + REQUEST_AUTOLOCK (request); + + impl_request = xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, &error); + if (!impl_request) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request (request, impl_request); + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + xdp_filter_options (arg_options, &opt_builder, + prepare_print_options, G_N_ELEMENTS (prepare_print_options), NULL); + xdp_dbus_impl_print_call_prepare_print (impl, + request->id, + app_id, + arg_parent_window, + arg_title, + arg_settings, + arg_page_setup, + g_variant_builder_end (&opt_builder), + NULL, + prepare_print_done, + g_object_ref (request)); + + xdp_dbus_print_complete_prepare_print (object, invocation, request->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +print_iface_init (XdpDbusPrintIface *iface) +{ + iface->handle_print = handle_print; + iface->handle_prepare_print = handle_prepare_print; +} + +static void +print_init (Print *print) +{ + xdp_dbus_print_set_version (XDP_DBUS_PRINT (print), 1); +} + +static void +print_class_init (PrintClass *klass) +{ +} + +GDBusInterfaceSkeleton * +print_create (GDBusConnection *connection, + const char *dbus_name, + gpointer lockdown_proxy) +{ + g_autoptr(GError) error = NULL; + + lockdown = lockdown_proxy; + + impl = xdp_dbus_impl_print_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_NONE, + dbus_name, + DESKTOP_PORTAL_OBJECT_PATH, + NULL, + &error); + if (impl == NULL) + { + g_warning ("Failed to create print proxy: %s", error->message); + return NULL; + } + + g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (impl), G_MAXINT); + + print = g_object_new (print_get_type (), NULL); + + return G_DBUS_INTERFACE_SKELETON (print); +} diff --git a/src/print.h b/src/print.h new file mode 100644 index 0000000..1a4ca50 --- /dev/null +++ b/src/print.h @@ -0,0 +1,27 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +#pragma once + +#include + +GDBusInterfaceSkeleton * print_create (GDBusConnection *connection, + const char *dbus_name, + gpointer lockdown); diff --git a/src/proxy-resolver.c b/src/proxy-resolver.c new file mode 100644 index 0000000..5a5ea19 --- /dev/null +++ b/src/proxy-resolver.c @@ -0,0 +1,112 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +#include "config.h" + +#include +#include + +#include "proxy-resolver.h" +#include "request.h" +#include "xdp-dbus.h" +#include "xdp-utils.h" + +typedef struct _ProxyResolver ProxyResolver; +typedef struct _ProxyResolverClass ProxyResolverClass; + +struct _ProxyResolver +{ + XdpDbusProxyResolverSkeleton parent_instance; + + GProxyResolver *resolver; +}; + +struct _ProxyResolverClass +{ + XdpDbusProxyResolverSkeletonClass parent_class; +}; + +static ProxyResolver *proxy_resolver; + +GType proxy_resolver_get_type (void) G_GNUC_CONST; +static void proxy_resolver_iface_init (XdpDbusProxyResolverIface *iface); + +G_DEFINE_TYPE_WITH_CODE (ProxyResolver, proxy_resolver, + XDP_DBUS_TYPE_PROXY_RESOLVER_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_PROXY_RESOLVER, + proxy_resolver_iface_init)); + +static gboolean +proxy_resolver_handle_lookup (XdpDbusProxyResolver *object, + GDBusMethodInvocation *invocation, + const char *arg_uri) +{ + ProxyResolver *resolver = (ProxyResolver *)object; + Request *request = request_from_invocation (invocation); + + if (!xdp_app_info_has_network (request->app_info)) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "This call is not available inside the sandbox"); + } + else + { + g_auto (GStrv) proxies = NULL; + GError *error = NULL; + + proxies = g_proxy_resolver_lookup (resolver->resolver, arg_uri, NULL, &error); + if (error) + g_dbus_method_invocation_take_error (invocation, error); + else + g_dbus_method_invocation_return_value (invocation, + g_variant_new ("(^as)", proxies)); + } + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +proxy_resolver_iface_init (XdpDbusProxyResolverIface *iface) +{ + iface->handle_lookup = proxy_resolver_handle_lookup; +} + +static void +proxy_resolver_init (ProxyResolver *resolver) +{ + resolver->resolver = g_proxy_resolver_get_default (); + + xdp_dbus_proxy_resolver_set_version (XDP_DBUS_PROXY_RESOLVER (resolver), 1); +} + +static void +proxy_resolver_class_init (ProxyResolverClass *klass) +{ +} + +GDBusInterfaceSkeleton * +proxy_resolver_create (GDBusConnection *connection) +{ + proxy_resolver = g_object_new (proxy_resolver_get_type (), NULL); + + return G_DBUS_INTERFACE_SKELETON (proxy_resolver); +} diff --git a/src/proxy-resolver.h b/src/proxy-resolver.h new file mode 100644 index 0000000..ea52393 --- /dev/null +++ b/src/proxy-resolver.h @@ -0,0 +1,26 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + + +#pragma once + +#include + +GDBusInterfaceSkeleton * proxy_resolver_create (GDBusConnection *connection); diff --git a/src/realtime.c b/src/realtime.c new file mode 100644 index 0000000..af47347 --- /dev/null +++ b/src/realtime.c @@ -0,0 +1,310 @@ +/* + * Copyright © 2021 Igalia S.L. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Patrick Griffis + */ + +#include "config.h" + +#include +#include + +#include "realtime.h" +#include "request.h" +#include "permissions.h" +#include "xdp-dbus.h" +#include "xdp-utils.h" + +#define PERMISSION_TABLE "realtime" +#define PERMISSION_ID "realtime" + +typedef struct _Realtime Realtime; +typedef struct _RealtimeClass RealtimeClass; + +struct _Realtime +{ + XdpDbusRealtimeSkeleton parent_instance; + GDBusProxy *rtkit_proxy; +}; + +struct _RealtimeClass +{ + XdpDbusRealtimeSkeletonClass parent_class; +}; + +static Realtime *realtime; + +GType realtime_get_type (void) G_GNUC_CONST; +static void realtime_iface_init (XdpDbusRealtimeIface *iface); + +G_DEFINE_TYPE_WITH_CODE (Realtime, realtime, XDP_DBUS_TYPE_REALTIME_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_REALTIME, + realtime_iface_init)); + +static gboolean +map_pid_if_needed (XdpAppInfo *app_info, pid_t *pid, pid_t *tid, GError **error) +{ + if (!xdp_app_info_is_host (app_info)) + { + if (!xdp_app_info_map_pids (app_info, pid, 1, error)) + { + g_prefix_error (error, "Could not map pid: "); + g_warning ("Realtime error: %s", (*error)->message); + return FALSE; + } + if (!xdp_app_info_map_tids (app_info, *pid, tid, 1, error)) + { + g_prefix_error (error, "Could not map tid: "); + g_warning ("Realtime error: %s", (*error)->message); + return FALSE; + } + } + + return TRUE; +} + +static void +on_call_ready (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GError) error = NULL; + g_autoptr(GDBusMethodInvocation) invocation = g_steal_pointer (&user_data); + g_autoptr(GVariant) response = NULL; + + response = g_dbus_proxy_call_finish (G_DBUS_PROXY (source_object), + result, + &error); + + if (error) + g_dbus_method_invocation_return_gerror (invocation, error); + else + g_dbus_method_invocation_return_value (invocation, g_variant_new ("()")); +} + +static gboolean +handle_make_thread_realtime_with_pid (XdpDbusRealtime *object, + GDBusMethodInvocation *invocation, + guint64 process, + guint64 thread, + guint32 priority) +{ + g_autoptr (GError) error = NULL; + Request *request = request_from_invocation (invocation); + pid_t pids[1] = { process }; + pid_t tids[1] = { thread }; + const char *app_id = xdp_app_info_get_id (request->app_info); + Permission permission; + + if (!realtime->rtkit_proxy) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_FAILED, + "RealtimeKit was not found"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + permission = get_permission_sync (app_id, PERMISSION_TABLE, PERMISSION_ID); + if (permission == PERMISSION_NO) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Permission denied"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + if (!map_pid_if_needed (request->app_info, pids, tids, &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_dbus_proxy_call (G_DBUS_PROXY (realtime->rtkit_proxy), + "MakeThreadRealtimeWithPID", + g_variant_new ("(ttu)", pids[0], tids[0], priority), + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + on_call_ready, + g_object_ref (invocation)); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_make_thread_high_priority_with_pid (XdpDbusRealtime *object, + GDBusMethodInvocation *invocation, + guint64 process, + guint64 thread, + gint32 priority) +{ + g_autoptr (GError) error = NULL; + Request *request = request_from_invocation (invocation); + pid_t pids[1] = { process }; + pid_t tids[1] = { thread }; + const char *app_id = xdp_app_info_get_id (request->app_info); + Permission permission; + + if (!realtime->rtkit_proxy) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_FAILED, + "RealtimeKit was not found"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + permission = get_permission_sync (app_id, PERMISSION_TABLE, PERMISSION_ID); + if (permission == PERMISSION_NO) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Permission denied"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + if (!map_pid_if_needed (request->app_info, pids, tids, &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_dbus_proxy_call (G_DBUS_PROXY (realtime->rtkit_proxy), + "MakeThreadHighPriorityWithPID", + g_variant_new ("(tti)", pids[0], tids[0], priority), + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + on_call_ready, + g_object_ref (invocation)); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +realtime_iface_init (XdpDbusRealtimeIface *iface) +{ + iface->handle_make_thread_realtime_with_pid = handle_make_thread_realtime_with_pid; + iface->handle_make_thread_high_priority_with_pid = handle_make_thread_high_priority_with_pid; +} + +static void +realtime_init (Realtime *realtime) +{ + xdp_dbus_realtime_set_version (XDP_DBUS_REALTIME (realtime), 1); +} + +static void +realtime_finalize (GObject *object) +{ + Realtime *self = (Realtime *) object; + + g_clear_object (&self->rtkit_proxy); + + G_OBJECT_CLASS (realtime_parent_class)->finalize (object); +} + +static void +realtime_class_init (RealtimeClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = realtime_finalize; +} + +static void +load_all_properties (GDBusProxy *proxy) +{ + const char * properties[] = { "MaxRealtimePriority", "MinNiceLevel", "RTTimeUSecMax" }; + enum prop_type { MAX_REALTIME_PRIORITY, MIN_NICE_LEVEL, RTTIME_USEC_MAX }; + + for (guint i = 0; i < G_N_ELEMENTS (properties); ++i) + { + GVariant *result; + GVariant *parameters; + GError *error = NULL; + + parameters = g_variant_new ("(ss)", "org.freedesktop.RealtimeKit1", properties[i]); + result = g_dbus_proxy_call_sync (proxy, + "org.freedesktop.DBus.Properties.Get", + g_steal_pointer (¶meters), + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + &error); + + if (error) + { + g_warning ("Failed to load RealtimeKit property: %s", error->message); + g_error_free (error); + } + else + { + GVariant *value; + g_variant_get (result, "(v)", &value); + + if (i == MAX_REALTIME_PRIORITY) + xdp_dbus_realtime_set_max_realtime_priority (XDP_DBUS_REALTIME (realtime), + g_variant_get_int32 (value)); + else if (i == MIN_NICE_LEVEL) + xdp_dbus_realtime_set_min_nice_level (XDP_DBUS_REALTIME (realtime), + g_variant_get_int32 (value)); + else if (i == RTTIME_USEC_MAX) + xdp_dbus_realtime_set_rttime_usec_max (XDP_DBUS_REALTIME (realtime), + g_variant_get_int64 (value)); + else + g_assert_not_reached (); + + g_dbus_proxy_set_cached_property (proxy, properties[i], value); + g_variant_unref (value); + g_variant_unref (result); + } + } +} + +GDBusInterfaceSkeleton * +realtime_create (GDBusConnection *connection) +{ + GDBusProxy *rtkit_proxy = NULL; + g_autoptr (GError) error = NULL; + + rtkit_proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + "org.freedesktop.RealtimeKit1", + "/org/freedesktop/RealtimeKit1", + "org.freedesktop.RealtimeKit1", + NULL, + &error); + if (!rtkit_proxy) + { + /* We continue on so the realtime interface remains exported, + * however it will fail to do anything */ + g_warning ("Failed to create RealtimeKit proxy: %s", error->message); + } + + realtime = g_object_new (realtime_get_type (), NULL); + realtime->rtkit_proxy = g_steal_pointer (&rtkit_proxy); + + if (realtime->rtkit_proxy) + load_all_properties (realtime->rtkit_proxy); + + return G_DBUS_INTERFACE_SKELETON (realtime); +} diff --git a/src/realtime.h b/src/realtime.h new file mode 100644 index 0000000..f35ee71 --- /dev/null +++ b/src/realtime.h @@ -0,0 +1,25 @@ +/* + * Copyright © 2021 Igalia S.L. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Patrick Griffis + */ + +#pragma once + +#include + +GDBusInterfaceSkeleton * realtime_create (GDBusConnection *connection); diff --git a/src/remote-desktop.c b/src/remote-desktop.c new file mode 100644 index 0000000..360db51 --- /dev/null +++ b/src/remote-desktop.c @@ -0,0 +1,1684 @@ +/* + * Copyright © 2017-2018 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + */ + +#include "config.h" + +#include "remote-desktop.h" +#include "screen-cast.h" +#include "request.h" +#include "restore-token.h" +#include "pipewire.h" +#include "call.h" +#include "session.h" +#include "xdp-dbus.h" +#include "xdp-impl-dbus.h" +#include "xdp-utils.h" + +#include +#include + +#define REMOTE_DESKTOP_TABLE "remote-desktop" + +typedef struct _RemoteDesktop RemoteDesktop; +typedef struct _RemoteDesktopClass RemoteDesktopClass; + +struct _RemoteDesktop +{ + XdpDbusRemoteDesktopSkeleton parent_instance; +}; + +struct _RemoteDesktopClass +{ + XdpDbusRemoteDesktopSkeletonClass parent_class; +}; + +static XdpDbusImplRemoteDesktop *impl; +static RemoteDesktop *remote_desktop; + +GType remote_desktop_get_type (void) G_GNUC_CONST; +static void remote_desktop_iface_init (XdpDbusRemoteDesktopIface *iface); + +static GQuark quark_request_session; + +G_DEFINE_TYPE_WITH_CODE (RemoteDesktop, remote_desktop, + XDP_DBUS_TYPE_REMOTE_DESKTOP_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_REMOTE_DESKTOP, + remote_desktop_iface_init)) + +typedef enum _RemoteDesktopSessionState +{ + REMOTE_DESKTOP_SESSION_STATE_INIT, + REMOTE_DESKTOP_SESSION_STATE_STARTED, + REMOTE_DESKTOP_SESSION_STATE_CLOSED +} RemoteDesktopSessionState; + +typedef enum _DeviceType +{ + DEVICE_TYPE_NONE = 0, + DEVICE_TYPE_KEYBOARD = 1 << 0, + DEVICE_TYPE_POINTER = 1 << 1, + DEVICE_TYPE_TOUCHSCREEN = 1 << 2, +} DeviceType; + +typedef struct _RemoteDesktopSession +{ + Session parent; + + RemoteDesktopSessionState state; + + DeviceType shared_devices; + + GList *streams; + + gboolean clipboard_requested; + + gboolean devices_selected; + + gboolean sources_selected; + + gboolean clipboard_enabled; + + gboolean uses_eis; + + char *restore_token; + PersistMode persist_mode; + GVariant *restore_data; +} RemoteDesktopSession; + +typedef struct _RemoteDesktopSessionClass +{ + SessionClass parent_class; +} RemoteDesktopSessionClass; + +GType remote_desktop_session_get_type (void); + +G_DEFINE_TYPE (RemoteDesktopSession, remote_desktop_session, session_get_type ()) + +gboolean +is_remote_desktop_session (Session *session) +{ + return G_TYPE_CHECK_INSTANCE_TYPE (session, + remote_desktop_session_get_type ()); +} + +gboolean +remote_desktop_session_can_select_sources (RemoteDesktopSession *session) +{ + if (session->sources_selected) + return FALSE; + + switch (session->state) + { + case REMOTE_DESKTOP_SESSION_STATE_INIT: + return TRUE; + case REMOTE_DESKTOP_SESSION_STATE_STARTED: + case REMOTE_DESKTOP_SESSION_STATE_CLOSED: + return FALSE; + } + + g_assert_not_reached (); +} + +gboolean +remote_desktop_session_can_select_devices (RemoteDesktopSession *session) +{ + if (session->devices_selected) + return FALSE; + + switch (session->state) + { + case REMOTE_DESKTOP_SESSION_STATE_INIT: + return TRUE; + case REMOTE_DESKTOP_SESSION_STATE_STARTED: + case REMOTE_DESKTOP_SESSION_STATE_CLOSED: + return FALSE; + } + + g_assert_not_reached (); +} + +gboolean +remote_desktop_session_can_request_clipboard (RemoteDesktopSession *session) +{ + if (session->clipboard_requested) + return FALSE; + + if (xdp_dbus_impl_remote_desktop_get_version (impl) < 2) + return FALSE; + + switch (session->state) + { + case REMOTE_DESKTOP_SESSION_STATE_INIT: + return TRUE; + case REMOTE_DESKTOP_SESSION_STATE_STARTED: + case REMOTE_DESKTOP_SESSION_STATE_CLOSED: + return FALSE; + } + + g_assert_not_reached (); +} + +GList * +remote_desktop_session_get_streams (RemoteDesktopSession *session) +{ + return session->streams; +} + +void +remote_desktop_session_sources_selected (RemoteDesktopSession *session) +{ + session->sources_selected = TRUE; +} + +gboolean +remote_desktop_session_is_clipboard_enabled (RemoteDesktopSession *session) +{ + return session->clipboard_enabled; +} + +void +remote_desktop_session_clipboard_requested (RemoteDesktopSession *session) +{ + session->clipboard_requested = TRUE; +} + +static RemoteDesktopSession * +remote_desktop_session_new (GVariant *options, + Request *request, + GError **error) +{ + Session *session; + GDBusInterfaceSkeleton *interface_skeleton = + G_DBUS_INTERFACE_SKELETON (request); + const char *session_token; + GDBusConnection *connection = + g_dbus_interface_skeleton_get_connection (interface_skeleton); + GDBusConnection *impl_connection = + g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)); + const char *impl_dbus_name = g_dbus_proxy_get_name (G_DBUS_PROXY (impl)); + + session_token = lookup_session_token (options); + session = g_initable_new (remote_desktop_session_get_type (), NULL, error, + "sender", request->sender, + "app-id", xdp_app_info_get_id (request->app_info), + "token", session_token, + "connection", connection, + "impl-connection", impl_connection, + "impl-dbus-name", impl_dbus_name, + NULL); + + if (session) + g_debug ("remote desktop session owned by '%s' created", session->sender); + + return (RemoteDesktopSession *)session; +} + +static void +create_session_done (GObject *source_object, + GAsyncResult *res, + gpointer data) +{ + g_autoptr(Request) request = data; + Session *session; + guint response = 2; + gboolean should_close_session; + GVariantBuilder results_builder; + g_autoptr(GError) error = NULL; + + REQUEST_AUTOLOCK (request); + + session = g_object_get_qdata (G_OBJECT (request), quark_request_session); + + g_variant_builder_init (&results_builder, G_VARIANT_TYPE_VARDICT); + SESSION_AUTOLOCK_UNREF (g_object_ref (session)); + g_object_set_qdata (G_OBJECT (request), quark_request_session, NULL); + + if (!xdp_dbus_impl_remote_desktop_call_create_session_finish (impl, + &response, + NULL, + res, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("A backend call failed: %s", error->message); + should_close_session = TRUE; + goto out; + } + if (request->exported && response == 0) + { + if (!session_export (session, &error)) + { + g_warning ("Failed to export session: %s", error->message); + response = 2; + should_close_session = TRUE; + goto out; + } + + should_close_session = FALSE; + session_register (session); + } + else + { + should_close_session = TRUE; + } + + g_variant_builder_add (&results_builder, "{sv}", + "session_handle", g_variant_new ("s", session->id)); + +out: + if (request->exported) + { + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + response, + g_variant_builder_end (&results_builder)); + request_unexport (request); + } + else + { + g_variant_builder_clear (&results_builder); + } + + if (should_close_session) + session_close (session, FALSE); +} + +static gboolean +handle_create_session (XdpDbusRemoteDesktop *object, + GDBusMethodInvocation *invocation, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + g_autoptr(GError) error = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + Session *session; + GVariantBuilder options_builder; + GVariant *options; + + REQUEST_AUTOLOCK (request); + + impl_request = + xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, &error); + if (!impl_request) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request (request, impl_request); + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + session = (Session *)remote_desktop_session_new (arg_options, request, &error); + if (!session) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + options = g_variant_builder_end (&options_builder); + + g_object_set_qdata_full (G_OBJECT (request), + quark_request_session, + g_object_ref (session), + g_object_unref); + + xdp_dbus_impl_remote_desktop_call_create_session (impl, + request->id, + session->id, + xdp_app_info_get_id (request->app_info), + options, + NULL, + create_session_done, + g_object_ref (request)); + + xdp_dbus_remote_desktop_complete_create_session (object, invocation, request->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +select_devices_done (GObject *source_object, + GAsyncResult *res, + gpointer data) +{ + g_autoptr(Request) request = data; + Session *session; + guint response = 2; + gboolean should_close_session; + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) results = NULL; + + REQUEST_AUTOLOCK (request); + + session = g_object_get_qdata (G_OBJECT (request), quark_request_session); + SESSION_AUTOLOCK_UNREF (g_object_ref (session)); + g_object_set_qdata (G_OBJECT (request), quark_request_session, NULL); + + if (!xdp_dbus_impl_remote_desktop_call_select_devices_finish (impl, + &response, + &results, + res, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("A backend call failed: %s", error->message); + } + + should_close_session = !request->exported || response != 0; + + if (request->exported) + { + if (!results) + { + GVariantBuilder results_builder; + + g_variant_builder_init (&results_builder, G_VARIANT_TYPE_VARDICT); + results = g_variant_ref_sink (g_variant_builder_end (&results_builder)); + } + + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + response, results); + request_unexport (request); + } + + if (should_close_session) + { + session_close (session, TRUE); + } + else if (!session->closed) + { + RemoteDesktopSession *remote_desktop_session = (RemoteDesktopSession *)session; + + remote_desktop_session->devices_selected = TRUE; + } +} + +static gboolean +validate_device_types (const char *key, + GVariant *value, + GVariant *options, + GError **error) +{ + guint32 types = g_variant_get_uint32 (value); + + if ((types & ~(1 | 2 | 4)) != 0) + { + g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Unsupported device type: %x", types & ~(1 | 2 | 4)); + return FALSE; + } + + return TRUE; +} + +static gboolean +validate_restore_token (const char *key, + GVariant *value, + GVariant *options, + GError **error) +{ + const char *restore_token = g_variant_get_string (value, NULL); + + if (!g_uuid_string_is_valid (restore_token)) + { + g_set_error (error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Restore token is not a valid UUID string"); + return FALSE; + } + + return TRUE; +} + +static gboolean +validate_persist_mode (const char *key, + GVariant *value, + GVariant *options, + GError **error) +{ + uint32_t mode = g_variant_get_uint32 (value); + + if (mode > PERSIST_MODE_PERSISTENT) + { + g_set_error (error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Invalid persist mode %x", mode); + return FALSE; + } + + return TRUE; +} + +static XdpOptionKey remote_desktop_select_devices_options[] = { + { "types", G_VARIANT_TYPE_UINT32, validate_device_types }, + { "restore_token", G_VARIANT_TYPE_STRING, validate_restore_token }, + { "persist_mode", G_VARIANT_TYPE_UINT32, validate_persist_mode }, +}; + +static gboolean +replace_remote_desktop_restore_token_with_data (Session *session, + GVariant **in_out_options, + GError **error) +{ + RemoteDesktopSession *remote_desktop_session = (RemoteDesktopSession *) session; + g_autoptr(GVariant) options = NULL; + PersistMode persist_mode; + + options = *in_out_options; + + if (!g_variant_lookup (options, "persist_mode", "u", &persist_mode)) + persist_mode = PERSIST_MODE_NONE; + + remote_desktop_session->persist_mode = persist_mode; + xdp_session_persistence_replace_restore_token_with_data (session, + REMOTE_DESKTOP_TABLE, + in_out_options, + &remote_desktop_session->restore_token); + + return TRUE; +} + +static gboolean +handle_select_devices (XdpDbusRemoteDesktop *object, + GDBusMethodInvocation *invocation, + const char *arg_session_handle, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + Session *session; + RemoteDesktopSession *remote_desktop_session; + g_autoptr(GError) error = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + GVariantBuilder options_builder; + GVariant *options; + + REQUEST_AUTOLOCK (request); + + session = acquire_session (arg_session_handle, request); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + remote_desktop_session = (RemoteDesktopSession *)session; + + if (!remote_desktop_session_can_select_devices (remote_desktop_session)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid state"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + impl_request = + xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, &error); + if (!impl_request) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request (request, impl_request); + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &options_builder, + remote_desktop_select_devices_options, + G_N_ELEMENTS (remote_desktop_select_devices_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + options = g_variant_builder_end (&options_builder); + + /* If 'restore_token' is passed, lookup the corresponding data in the + * permission store and / or the GHashTable with transient permissions. + * Portal implementations do not have access to the restore token. + */ + if (!replace_remote_desktop_restore_token_with_data (session, &options, &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_object_set_qdata_full (G_OBJECT (request), + quark_request_session, + g_object_ref (session), + g_object_unref); + + xdp_dbus_impl_remote_desktop_call_select_devices (impl, + request->id, + arg_session_handle, + xdp_app_info_get_id (request->app_info), + options, + NULL, + select_devices_done, + g_object_ref (request)); + + xdp_dbus_remote_desktop_complete_select_devices (object, invocation, request->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +replace_restore_remote_desktop_data_with_token (RemoteDesktopSession *remote_desktop_session, + GVariant **in_out_results) +{ + xdp_session_persistence_replace_restore_data_with_token ((Session *) remote_desktop_session, + REMOTE_DESKTOP_TABLE, + in_out_results, + &remote_desktop_session->persist_mode, + &remote_desktop_session->restore_token, + &remote_desktop_session->restore_data); +} + +static gboolean +process_results (RemoteDesktopSession *remote_desktop_session, + GVariant **in_out_results, + GError **error) +{ + g_autoptr(GVariantIter) streams_iter = NULL; + GVariant *results = *in_out_results; + uint32_t devices = 0; + gboolean clipboard_enabled = FALSE; + + if (g_variant_lookup (results, "streams", "a(ua{sv})", &streams_iter)) + { + remote_desktop_session->streams = + collect_screen_cast_stream_data (streams_iter); + } + + if (g_variant_lookup (results, "devices", "u", &devices)) + remote_desktop_session->shared_devices = devices; + + if (g_variant_lookup (results, "clipboard_enabled", "b", &clipboard_enabled)) + remote_desktop_session->clipboard_enabled = clipboard_enabled; + + replace_restore_remote_desktop_data_with_token (remote_desktop_session, + in_out_results); + + return TRUE; +} + +static void +start_done (GObject *source_object, + GAsyncResult *res, + gpointer data) +{ + g_autoptr(Request) request = data; + Session *session; + RemoteDesktopSession *remote_desktop_session; + guint response = 2; + gboolean should_close_session; + GVariant *results = NULL; + g_autoptr(GError) error = NULL; + + REQUEST_AUTOLOCK (request); + + session = g_object_get_qdata (G_OBJECT (request), quark_request_session); + remote_desktop_session = (RemoteDesktopSession *)session; + SESSION_AUTOLOCK_UNREF (g_object_ref (session)); + g_object_set_qdata (G_OBJECT (request), quark_request_session, NULL); + + if (!xdp_dbus_impl_remote_desktop_call_start_finish (impl, + &response, + &results, + res, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("A backend call failed: %s", error->message); + } + + should_close_session = !request->exported || response != 0; + + if (request->exported) + { + if (response == 0) + { + if (!process_results (remote_desktop_session, &results, &error)) + { + g_warning ("Could not start remote desktop session: %s", + error->message); + g_clear_error (&error); + g_clear_pointer (&results, g_variant_unref); + response = 2; + should_close_session = TRUE; + } + } + else + { + GVariantBuilder results_builder; + + g_variant_builder_init (&results_builder, G_VARIANT_TYPE_VARDICT); + results = g_variant_builder_end (&results_builder); + } + + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + response, results); + request_unexport (request); + } + + if (should_close_session) + { + session_close (session, TRUE); + } + else if (!session->closed) + { + g_debug ("remote desktop session owned by '%s' started", session->sender); + remote_desktop_session->state = REMOTE_DESKTOP_SESSION_STATE_STARTED; + } +} + +static gboolean +handle_start (XdpDbusRemoteDesktop *object, + GDBusMethodInvocation *invocation, + const char *arg_session_handle, + const char *arg_parent_window, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + Session *session; + RemoteDesktopSession *remote_desktop_session; + g_autoptr(GError) error = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + GVariantBuilder options_builder; + GVariant *options; + + REQUEST_AUTOLOCK (request); + + session = acquire_session (arg_session_handle, request); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + remote_desktop_session = (RemoteDesktopSession *)session; + switch (remote_desktop_session->state) + { + case REMOTE_DESKTOP_SESSION_STATE_INIT: + break; + case REMOTE_DESKTOP_SESSION_STATE_STARTED: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Can only start once"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + case REMOTE_DESKTOP_SESSION_STATE_CLOSED: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_object_set_data_full (G_OBJECT (request), + "window", g_strdup (arg_parent_window), g_free); + + impl_request = + xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, &error); + if (!impl_request) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request (request, impl_request); + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + options = g_variant_builder_end (&options_builder); + + g_object_set_qdata_full (G_OBJECT (request), + quark_request_session, + g_object_ref (session), + g_object_unref); + + xdp_dbus_impl_remote_desktop_call_start (impl, + request->id, + arg_session_handle, + xdp_app_info_get_id (request->app_info), + arg_parent_window, + options, + NULL, + start_done, + g_object_ref (request)); + + xdp_dbus_remote_desktop_complete_start (object, invocation, request->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +check_notify (Session *session, + DeviceType device_type) +{ + RemoteDesktopSession *remote_desktop_session = (RemoteDesktopSession *)session; + + if (!remote_desktop_session->devices_selected || remote_desktop_session->uses_eis) + return FALSE; + + switch (remote_desktop_session->state) + { + case REMOTE_DESKTOP_SESSION_STATE_STARTED: + break; + case REMOTE_DESKTOP_SESSION_STATE_INIT: + case REMOTE_DESKTOP_SESSION_STATE_CLOSED: + return FALSE; + } + + if ((remote_desktop_session->shared_devices & device_type) == 0) + return FALSE; + + return TRUE; +} + +static gboolean +check_position (Session *session, + uint32_t stream, + double x, + double y) +{ + RemoteDesktopSession *remote_desktop_session = (RemoteDesktopSession *)session; + GList *l; + + for (l = remote_desktop_session->streams; l; l = l->next) + { + ScreenCastStream *stream = l->data; + int32_t width, height; + + screen_cast_stream_get_size (stream, &width, &height); + + if (x >= 0.0 && x < width && + y >= 0.0 && y < height) + return TRUE; + } + + return FALSE; +} + +static XdpOptionKey remote_desktop_notify_options[] = { +}; + +static gboolean +handle_notify_pointer_motion (XdpDbusRemoteDesktop *object, + GDBusMethodInvocation *invocation, + const char *arg_session_handle, + GVariant *arg_options, + double dx, + double dy) +{ + Call *call = call_from_invocation (invocation); + Session *session; + GVariantBuilder options_builder; + GVariant *options; + g_autoptr(GError) error = NULL; + + session = acquire_session_from_call (arg_session_handle, call); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + if (!check_notify (session, DEVICE_TYPE_POINTER)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Session is not allowed to call NotifyPointer methods"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &options_builder, + remote_desktop_notify_options, + G_N_ELEMENTS (remote_desktop_notify_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + options = g_variant_builder_end (&options_builder); + + xdp_dbus_impl_remote_desktop_call_notify_pointer_motion (impl, + session->id, + options, + dx, dy, + NULL, NULL, NULL); + + xdp_dbus_remote_desktop_complete_notify_pointer_motion (object, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_notify_pointer_motion_absolute (XdpDbusRemoteDesktop *object, + GDBusMethodInvocation *invocation, + const char *arg_session_handle, + GVariant *arg_options, + uint32_t stream, + double x, + double y) +{ + Call *call = call_from_invocation (invocation); + Session *session; + GVariantBuilder options_builder; + GVariant *options; + g_autoptr(GError) error = NULL; + + session = acquire_session_from_call (arg_session_handle, call); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + if (!check_notify (session, DEVICE_TYPE_POINTER)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Session is not allowed to call NotifyPointer methods"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + if (!check_position (session, stream, x, y)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid position"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &options_builder, + remote_desktop_notify_options, + G_N_ELEMENTS (remote_desktop_notify_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + options = g_variant_builder_end (&options_builder); + + xdp_dbus_impl_remote_desktop_call_notify_pointer_motion_absolute (impl, + session->id, + options, + stream, + x, y, + NULL, NULL, NULL); + + xdp_dbus_remote_desktop_complete_notify_pointer_motion_absolute (object, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_notify_pointer_button (XdpDbusRemoteDesktop *object, + GDBusMethodInvocation *invocation, + const char *arg_session_handle, + GVariant *arg_options, + int32_t button, + uint32_t state) +{ + Call *call = call_from_invocation (invocation); + Session *session; + GVariantBuilder options_builder; + GVariant *options; + g_autoptr(GError) error = NULL; + + session = acquire_session_from_call (arg_session_handle, call); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + if (!check_notify (session, DEVICE_TYPE_POINTER)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Session is not allowed to call NotifyPointer methods"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &options_builder, + remote_desktop_notify_options, + G_N_ELEMENTS (remote_desktop_notify_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + options = g_variant_builder_end (&options_builder); + + xdp_dbus_impl_remote_desktop_call_notify_pointer_button (impl, + session->id, + options, + button, + state, + NULL, NULL, NULL); + + xdp_dbus_remote_desktop_complete_notify_pointer_button (object, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static XdpOptionKey remote_desktop_notify_pointer_axis_options[] = { + { "finish", G_VARIANT_TYPE_BOOLEAN, NULL }, +}; + +static gboolean +handle_notify_pointer_axis (XdpDbusRemoteDesktop *object, + GDBusMethodInvocation *invocation, + const char *arg_session_handle, + GVariant *arg_options, + double dx, + double dy) +{ + Call *call = call_from_invocation (invocation); + Session *session; + GVariantBuilder options_builder; + GVariant *options; + g_autoptr(GError) error = NULL; + + session = acquire_session_from_call (arg_session_handle, call); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + if (!check_notify (session, DEVICE_TYPE_POINTER)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Session is not allowed to call NotifyPointer methods"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &options_builder, + remote_desktop_notify_pointer_axis_options, + G_N_ELEMENTS (remote_desktop_notify_pointer_axis_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + options = g_variant_builder_end (&options_builder); + + xdp_dbus_impl_remote_desktop_call_notify_pointer_axis (impl, + session->id, + options, + dx, dy, + NULL, NULL, NULL); + + xdp_dbus_remote_desktop_complete_notify_pointer_axis (object, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_notify_pointer_axis_discrete (XdpDbusRemoteDesktop *object, + GDBusMethodInvocation *invocation, + const char *arg_session_handle, + GVariant *arg_options, + uint32_t axis, + int32_t steps) +{ + Call *call = call_from_invocation (invocation); + Session *session; + GVariantBuilder options_builder; + GVariant *options; + g_autoptr(GError) error = NULL; + + session = acquire_session_from_call (arg_session_handle, call); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + if (!check_notify (session, DEVICE_TYPE_POINTER)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Session is not allowed to call NotifyPointer methods"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &options_builder, + remote_desktop_notify_options, + G_N_ELEMENTS (remote_desktop_notify_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + options = g_variant_builder_end (&options_builder); + + xdp_dbus_impl_remote_desktop_call_notify_pointer_axis_discrete (impl, + session->id, + options, + axis, + steps, + NULL, NULL, NULL); + + xdp_dbus_remote_desktop_complete_notify_pointer_axis_discrete (object, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_notify_keyboard_keycode (XdpDbusRemoteDesktop *object, + GDBusMethodInvocation *invocation, + const char *arg_session_handle, + GVariant *arg_options, + int32_t keycode, + uint32_t state) +{ + Call *call = call_from_invocation (invocation); + Session *session; + GVariantBuilder options_builder; + GVariant *options; + g_autoptr(GError) error = NULL; + + session = acquire_session_from_call (arg_session_handle, call); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + if (!check_notify (session, DEVICE_TYPE_KEYBOARD)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Session is not allowed to call NotifyPointer methods"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &options_builder, + remote_desktop_notify_options, + G_N_ELEMENTS (remote_desktop_notify_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + options = g_variant_builder_end (&options_builder); + + xdp_dbus_impl_remote_desktop_call_notify_keyboard_keycode (impl, + session->id, + options, + keycode, + state, + NULL, NULL, NULL); + + xdp_dbus_remote_desktop_complete_notify_keyboard_keycode (object, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_notify_keyboard_keysym (XdpDbusRemoteDesktop *object, + GDBusMethodInvocation *invocation, + const char *arg_session_handle, + GVariant *arg_options, + int32_t keysym, + uint32_t state) +{ + Call *call = call_from_invocation (invocation); + Session *session; + GVariantBuilder options_builder; + GVariant *options; + g_autoptr(GError) error = NULL; + + session = acquire_session_from_call (arg_session_handle, call); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + if (!check_notify (session, DEVICE_TYPE_KEYBOARD)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Session is not allowed to call NotifyKeyboard methods"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &options_builder, + remote_desktop_notify_options, + G_N_ELEMENTS (remote_desktop_notify_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + options = g_variant_builder_end (&options_builder); + + xdp_dbus_impl_remote_desktop_call_notify_keyboard_keysym (impl, + session->id, + options, + keysym, + state, + NULL, NULL, NULL); + + xdp_dbus_remote_desktop_complete_notify_keyboard_keysym (object, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_notify_touch_down (XdpDbusRemoteDesktop *object, + GDBusMethodInvocation *invocation, + const char *arg_session_handle, + GVariant *arg_options, + uint32_t stream, + uint32_t slot, + double x, + double y) +{ + Call *call = call_from_invocation (invocation); + Session *session; + GVariantBuilder options_builder; + GVariant *options; + g_autoptr(GError) error = NULL; + + session = acquire_session_from_call (arg_session_handle, call); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + if (!check_notify (session, DEVICE_TYPE_TOUCHSCREEN)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Session is not allowed to call NotifyTouch methods"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + if (!check_position (session, stream, x, y)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid position"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &options_builder, + remote_desktop_notify_options, + G_N_ELEMENTS (remote_desktop_notify_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + options = g_variant_builder_end (&options_builder); + + xdp_dbus_impl_remote_desktop_call_notify_touch_down (impl, + session->id, + options, + stream, + slot, + x, y, + NULL, NULL, NULL); + + xdp_dbus_remote_desktop_complete_notify_touch_down (object, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_notify_touch_motion (XdpDbusRemoteDesktop *object, + GDBusMethodInvocation *invocation, + const char *arg_session_handle, + GVariant *arg_options, + uint32_t stream, + uint32_t slot, + double x, + double y) +{ + Call *call = call_from_invocation (invocation); + Session *session; + GVariantBuilder options_builder; + GVariant *options; + g_autoptr(GError) error = NULL; + + session = acquire_session_from_call (arg_session_handle, call); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + if (!check_notify (session, DEVICE_TYPE_TOUCHSCREEN)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Session is not allowed to call NotifyTouch methods"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + if (!check_position (session, stream, x, y)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid position"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &options_builder, + remote_desktop_notify_options, + G_N_ELEMENTS (remote_desktop_notify_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + options = g_variant_builder_end (&options_builder); + + xdp_dbus_impl_remote_desktop_call_notify_touch_motion (impl, + session->id, + options, + stream, + slot, + x, y, + NULL, NULL, NULL); + + xdp_dbus_remote_desktop_complete_notify_touch_motion (object, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_notify_touch_up (XdpDbusRemoteDesktop *object, + GDBusMethodInvocation *invocation, + const char *arg_session_handle, + GVariant *arg_options, + uint32_t slot) +{ + Call *call = call_from_invocation (invocation); + Session *session; + GVariantBuilder options_builder; + GVariant *options; + g_autoptr(GError) error = NULL; + + session = acquire_session_from_call (arg_session_handle, call); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + if (!check_notify (session, DEVICE_TYPE_TOUCHSCREEN)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Session is not allowed to call NotifyTouch methods"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &options_builder, + remote_desktop_notify_options, + G_N_ELEMENTS (remote_desktop_notify_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + options = g_variant_builder_end (&options_builder); + + xdp_dbus_impl_remote_desktop_call_notify_touch_up (impl, + session->id, + options, + slot, + NULL, NULL, NULL); + + xdp_dbus_remote_desktop_complete_notify_touch_up (object, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static XdpOptionKey remote_desktop_connect_to_eis_options[] = { +}; + +static gboolean +handle_connect_to_eis (XdpDbusRemoteDesktop *object, + GDBusMethodInvocation *invocation, + GUnixFDList *in_fd_list, + const char *arg_session_handle, + GVariant *arg_options) +{ + Call *call = call_from_invocation (invocation); + Session *session; + RemoteDesktopSession *remote_desktop_session; + g_autoptr(GUnixFDList) out_fd_list = NULL; + g_autoptr(GError) error = NULL; + GVariantBuilder options_builder; + GVariant *fd; + + session = acquire_session_from_call (arg_session_handle, call); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + if (!is_remote_desktop_session (session)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + remote_desktop_session = (RemoteDesktopSession *)session; + + if (remote_desktop_session->uses_eis) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Session is already connected"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + switch (remote_desktop_session->state) + { + case REMOTE_DESKTOP_SESSION_STATE_STARTED: + break; + case REMOTE_DESKTOP_SESSION_STATE_INIT: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Session is not ready"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + case REMOTE_DESKTOP_SESSION_STATE_CLOSED: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Session is already closed"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &options_builder, + remote_desktop_connect_to_eis_options, + G_N_ELEMENTS (remote_desktop_connect_to_eis_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + if (!xdp_dbus_impl_remote_desktop_call_connect_to_eis_sync (impl, + arg_session_handle, + xdp_app_info_get_id (call->app_info), + g_variant_builder_end (&options_builder), + in_fd_list, + &fd, + &out_fd_list, + NULL, + &error)) + { + g_warning ("Failed to ConnectToEIS: %s", error->message); + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + remote_desktop_session->uses_eis = TRUE; + + xdp_dbus_remote_desktop_complete_connect_to_eis (object, invocation, out_fd_list, fd); + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +remote_desktop_iface_init (XdpDbusRemoteDesktopIface *iface) +{ + iface->handle_create_session = handle_create_session; + iface->handle_select_devices = handle_select_devices; + iface->handle_start = handle_start; + + iface->handle_notify_pointer_motion = handle_notify_pointer_motion; + iface->handle_notify_pointer_motion_absolute = handle_notify_pointer_motion_absolute; + iface->handle_notify_pointer_button = handle_notify_pointer_button; + iface->handle_notify_pointer_axis = handle_notify_pointer_axis; + iface->handle_notify_pointer_axis_discrete = handle_notify_pointer_axis_discrete; + iface->handle_notify_keyboard_keycode = handle_notify_keyboard_keycode; + iface->handle_notify_keyboard_keysym = handle_notify_keyboard_keysym; + iface->handle_notify_touch_down = handle_notify_touch_down; + iface->handle_notify_touch_motion = handle_notify_touch_motion; + iface->handle_notify_touch_up = handle_notify_touch_up; + + iface->handle_connect_to_eis = handle_connect_to_eis; +} + +static void +sync_supported_device_types (RemoteDesktop *remote_desktop) +{ + unsigned int available_device_types; + + available_device_types = + xdp_dbus_impl_remote_desktop_get_available_device_types (impl); + xdp_dbus_remote_desktop_set_available_device_types (XDP_DBUS_REMOTE_DESKTOP (remote_desktop), + available_device_types); +} + +static void +on_supported_device_types_changed (GObject *gobject, + GParamSpec *pspec, + RemoteDesktop *remote_desktop) +{ + sync_supported_device_types (remote_desktop); +} + +static void +remote_desktop_init (RemoteDesktop *remote_desktop) +{ + xdp_dbus_remote_desktop_set_version (XDP_DBUS_REMOTE_DESKTOP (remote_desktop), 2); + + g_signal_connect (impl, "notify::supported-device-types", + G_CALLBACK (on_supported_device_types_changed), + remote_desktop); + sync_supported_device_types (remote_desktop); +} + +static void +remote_desktop_class_init (RemoteDesktopClass *klass) +{ +} + +GDBusInterfaceSkeleton * +remote_desktop_create (GDBusConnection *connection, + const char *dbus_name) +{ + g_autoptr(GError) error = NULL; + + impl = xdp_dbus_impl_remote_desktop_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_NONE, + dbus_name, + DESKTOP_PORTAL_OBJECT_PATH, + NULL, + &error); + if (impl == NULL) + { + g_warning ("Failed to create remote desktop proxy: %s", error->message); + return NULL; + } + + g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (impl), G_MAXINT); + + remote_desktop = g_object_new (remote_desktop_get_type (), NULL); + + return G_DBUS_INTERFACE_SKELETON (remote_desktop); +} + +static void +remote_desktop_session_close (Session *session) +{ + RemoteDesktopSession *remote_desktop_session = (RemoteDesktopSession *)session; + + remote_desktop_session->state = REMOTE_DESKTOP_SESSION_STATE_CLOSED; + + g_debug ("remote desktop session owned by '%s' closed", session->sender); +} + +static void +remote_desktop_session_finalize (GObject *object) +{ + RemoteDesktopSession *remote_desktop_session = (RemoteDesktopSession *)object; + + g_list_free_full (remote_desktop_session->streams, + (GDestroyNotify)screen_cast_stream_free); + + G_OBJECT_CLASS (remote_desktop_session_parent_class)->finalize (object); +} + +static void +remote_desktop_session_init (RemoteDesktopSession *remote_desktop_session) +{ +} + +static void +remote_desktop_session_class_init (RemoteDesktopSessionClass *klass) +{ + GObjectClass *object_class; + SessionClass *session_class; + + object_class = G_OBJECT_CLASS (klass); + object_class->finalize = remote_desktop_session_finalize; + + session_class = (SessionClass *)klass; + session_class->close = remote_desktop_session_close; + + quark_request_session = + g_quark_from_static_string ("-xdp-request-remote-desktop-session"); +} diff --git a/src/remote-desktop.h b/src/remote-desktop.h new file mode 100644 index 0000000..6c68c4d --- /dev/null +++ b/src/remote-desktop.h @@ -0,0 +1,43 @@ +/* + * Copyright © 2017-2018 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + */ + +#pragma once + +#include + +#include "session.h" +#include "screen-cast.h" + +typedef struct _RemoteDesktopSession RemoteDesktopSession; + +gboolean is_remote_desktop_session (Session *session); + +GList * remote_desktop_session_get_streams (RemoteDesktopSession *session); + +gboolean remote_desktop_session_can_select_sources (RemoteDesktopSession *session); + +gboolean remote_desktop_session_can_request_clipboard (RemoteDesktopSession *session); + +gboolean remote_desktop_session_is_clipboard_enabled (RemoteDesktopSession *session); + +void remote_desktop_session_sources_selected (RemoteDesktopSession *session); + +void remote_desktop_session_clipboard_requested (RemoteDesktopSession *session); + +GDBusInterfaceSkeleton * remote_desktop_create (GDBusConnection *connection, + const char *dbus_name); diff --git a/src/request.c b/src/request.c new file mode 100644 index 0000000..4216696 --- /dev/null +++ b/src/request.c @@ -0,0 +1,531 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Alexander Larsson + * Matthias Clasen + */ + +#include "request.h" +#include "xdp-utils.h" + +#include + +static void request_skeleton_iface_init (XdpDbusRequestIface *iface); + +G_DEFINE_TYPE_WITH_CODE (Request, request, XDP_DBUS_TYPE_REQUEST_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_REQUEST, + request_skeleton_iface_init)) + +static void +request_on_signal_response (XdpDbusRequest *object, + guint arg_response, + GVariant *arg_results) +{ + Request *request = (Request *)object; + XdpDbusRequestSkeleton *skeleton = XDP_DBUS_REQUEST_SKELETON (object); + GList *connections, *l; + GVariant *signal_variant; + + connections = g_dbus_interface_skeleton_get_connections (G_DBUS_INTERFACE_SKELETON (skeleton)); + + signal_variant = g_variant_ref_sink (g_variant_new ("(u@a{sv})", + arg_response, + arg_results)); + for (l = connections; l != NULL; l = l->next) + { + GDBusConnection *connection = l->data; + g_dbus_connection_emit_signal (connection, + request->sender, + g_dbus_interface_skeleton_get_object_path (G_DBUS_INTERFACE_SKELETON (skeleton)), + "org.freedesktop.portal.Request", + "Response", + signal_variant, + NULL); + } + g_variant_unref (signal_variant); + g_list_free_full (connections, g_object_unref); +} + +static gboolean +handle_close (XdpDbusRequest *object, + GDBusMethodInvocation *invocation) +{ + Request *request = (Request *)object; + g_autoptr(GError) error = NULL; + + g_debug ("Handling Close"); + REQUEST_AUTOLOCK (request); + + if (request->exported) + { + if (request->impl_request && + !xdp_dbus_impl_request_call_close_sync (request->impl_request, + NULL, &error)) + { + if (invocation) + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_unexport (request); + } + + if (invocation) + xdp_dbus_request_complete_close (XDP_DBUS_REQUEST (request), invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +request_skeleton_iface_init (XdpDbusRequestIface *iface) +{ + iface->handle_close = handle_close; + iface->response = request_on_signal_response; +} + +G_LOCK_DEFINE (requests); +static GHashTable *requests; + +static void +request_init (Request *request) +{ + g_mutex_init (&request->mutex); +} + +static void +request_finalize (GObject *object) +{ + Request *request = (Request *)object; + + G_LOCK (requests); + g_hash_table_remove (requests, request->id); + G_UNLOCK (requests); + + g_clear_object (&request->impl_request); + + g_free (request->sender); + g_free (request->id); + g_mutex_clear (&request->mutex); + xdp_app_info_unref (request->app_info); + + G_OBJECT_CLASS (request_parent_class)->finalize (object); +} + +static void +request_class_init (RequestClass *klass) +{ + GObjectClass *gobject_class; + + requests = g_hash_table_new_full (g_str_hash, g_str_equal, + NULL, NULL); + + gobject_class = G_OBJECT_CLASS (klass); + gobject_class->finalize = request_finalize; +} + +static gboolean +request_authorize_callback (GDBusInterfaceSkeleton *interface, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + const gchar *request_sender = user_data; + const gchar *sender = g_dbus_method_invocation_get_sender (invocation); + + if (strcmp (sender, request_sender) != 0) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Portal operation not allowed: Unmatched caller"); + return FALSE; + } + + return TRUE; +} + +/* This is a bit ugly - we need to know where the options vardict is + * in the parameters for each request. Instead of inventing some + * complicated mechanism for each implementation to provide that + * information, just hardcode it here for now. + * + * Note that the pointer returned by this function is good to use + * as long as the invocation object exists, since it points at data + * in the parameters variant. + */ +static const char * +get_token (GDBusMethodInvocation *invocation) +{ + const char *interface; + const char *method; + GVariant *parameters; + g_autoptr(GVariant) options = NULL; + const char *token = NULL; + + interface = g_dbus_method_invocation_get_interface_name (invocation); + method = g_dbus_method_invocation_get_method_name (invocation); + parameters = g_dbus_method_invocation_get_parameters (invocation); + + if (strcmp (interface, "org.freedesktop.portal.Account") == 0) + { + options = g_variant_get_child_value (parameters, 1); + } + else if (strcmp (interface, "org.freedesktop.portal.Device") == 0) + { + options = g_variant_get_child_value (parameters, 2); + } + else if (strcmp (interface, "org.freedesktop.portal.Email") == 0) + { + options = g_variant_get_child_value (parameters, 1); + } + else if (strcmp (interface, "org.freedesktop.portal.FileChooser") == 0) + { + options = g_variant_get_child_value (parameters, 2); + } + else if (strcmp (interface, "org.freedesktop.portal.Inhibit") == 0) + { + if (strcmp (method, "Inhibit") == 0) + options = g_variant_get_child_value (parameters, 2); + else if (strcmp (method, "CreateMonitor") == 0) + options = g_variant_get_child_value (parameters, 1); + } + else if (strcmp (interface, "org.freedesktop.portal.InputCapture") == 0) + { + options = g_variant_get_child_value (parameters, 1); + } + else if (strcmp (interface, "org.freedesktop.portal.NetworkMonitor") == 0) + { + // no methods + } + else if (strcmp (interface, "org.freedesktop.portal.Notification") == 0) + { + // no request objects + } + else if (strcmp (interface, "org.freedesktop.portal.OpenURI") == 0) + { + options = g_variant_get_child_value (parameters, 2); + } + else if (strcmp (interface, "org.freedesktop.portal.Print") == 0) + { + if (strcmp (method, "Print") == 0) + options = g_variant_get_child_value (parameters, 3); + else if (strcmp (method, "PreparePrint") == 0) + options = g_variant_get_child_value (parameters, 4); + } + else if (strcmp (interface, "org.freedesktop.portal.ProxyResolver") == 0) + { + // no request objects + } + else if (strcmp (interface, "org.freedesktop.portal.Screenshot") == 0) + { + options = g_variant_get_child_value (parameters, 1); + } + else if (strcmp (interface, "org.freedesktop.portal.ScreenCast") == 0) + { + if (strcmp (method, "CreateSession") == 0 ) + { + options = g_variant_get_child_value (parameters, 0); + } + else if (strcmp (method, "SelectSources") == 0) + { + options = g_variant_get_child_value (parameters, 1); + } + else if (strcmp (method, "Start") == 0) + { + options = g_variant_get_child_value (parameters, 2); + } + else + { + g_warning ("Support for %s::%s missing in %s", + interface, method, G_STRLOC); + } + } + else if (strcmp (interface, "org.freedesktop.portal.RemoteDesktop") == 0) + { + if (strcmp (method, "CreateSession") == 0 ) + { + options = g_variant_get_child_value (parameters, 0); + } + else if (strcmp (method, "SelectDevices") == 0) + { + options = g_variant_get_child_value (parameters, 1); + } + else if (strcmp (method, "Start") == 0) + { + options = g_variant_get_child_value (parameters, 2); + } + else + { + g_warning ("Support for %s::%s missing in %s", + interface, method, G_STRLOC); + } + } + else if (strcmp (interface, "org.freedesktop.portal.Clipboard") == 0) + { + // no request objects + } + else if (strcmp (interface, "org.freedesktop.portal.Location") == 0) + { + if (strcmp (method, "CreateSession") == 0 ) + { + options = g_variant_get_child_value (parameters, 0); + } + else if (strcmp (method, "SelectDetails") == 0) + { + options = g_variant_get_child_value (parameters, 1); + } + else if (strcmp (method, "Start") == 0) + { + options = g_variant_get_child_value (parameters, 2); + } + else + { + g_warning ("Support for %s::%s missing in %s", + interface, method, G_STRLOC); + } + } + else if (strcmp (interface, "org.freedesktop.portal.Settings") == 0) + { + // no request objects + } + else if (strcmp (interface, "org.freedesktop.portal.GameMode") == 0) + { + // no request objects + } + else if (strcmp (interface, "org.freedesktop.portal.Realtime") == 0) + { + // no request objects + } + else if (strcmp (interface, "org.freedesktop.portal.Trash") == 0) + { + // no request objects + } + else if (strcmp (interface, "org.freedesktop.portal.Background") == 0) + { + if (strcmp (method, "RequestBackground") == 0 ) + { + options = g_variant_get_child_value (parameters, 1); + } + } + else if (strcmp (interface, "org.freedesktop.portal.DynamicLauncher") == 0) + { + if (strcmp (method, "PrepareInstall") == 0 ) + { + options = g_variant_get_child_value (parameters, 3); + } + } + else if (strcmp (interface, "org.freedesktop.portal.Wallpaper") == 0) + { + options = g_variant_get_child_value (parameters, 2); + } + else if (strcmp (interface, "org.freedesktop.portal.Camera") == 0) + { + if (strcmp (method, "AccessCamera") == 0 ) + { + options = g_variant_get_child_value (parameters, 0); + } + else if (strcmp (method, "OpenPipewireRemote") == 0) + { + // no request objects + } + else + { + g_warning ("Support for %s::%s missing in %s", + interface, method, G_STRLOC); + } + } + else if (strcmp (interface, "org.freedesktop.portal.Secret") == 0) + { + options = g_variant_get_child_value (parameters, 1); + } + else if (strcmp (interface, "org.freedesktop.portal.GlobalShortcuts") == 0) + { + if (strcmp (method, "CreateSession") == 0 ) + { + options = g_variant_get_child_value (parameters, 0); + } + else if (strcmp (method, "BindShortcuts") == 0 ) + { + options = g_variant_get_child_value (parameters, 3); + } + else if (strcmp (method, "ListShortcuts") == 0 ) + { + options = g_variant_get_child_value (parameters, 1); + } + else + { + g_warning ("Support for %s::%s missing in %s", + interface, method, G_STRLOC); + } + } + else + { + g_print ("Support for %s missing in " G_STRLOC, interface); + } + + if (options) + g_variant_lookup (options, "handle_token", "&s", &token); + + return token ? token : "t"; +} + +void +request_init_invocation (GDBusMethodInvocation *invocation, XdpAppInfo *app_info) +{ + Request *request; + guint32 r; + char *id = NULL; + const char *token; + g_autofree char *sender = NULL; + int i; + + request = g_object_new (request_get_type (), NULL); + request->sender = g_strdup (g_dbus_method_invocation_get_sender (invocation)); + request->app_info = xdp_app_info_ref (app_info); + + g_object_set_data (G_OBJECT (request), "fd", GINT_TO_POINTER (-1)); + + token = get_token (invocation); + sender = g_strdup (request->sender + 1); + for (i = 0; sender[i]; i++) + if (sender[i] == '.') + sender[i] = '_'; + + id = g_strdup_printf ("/org/freedesktop/portal/desktop/request/%s/%s", sender, token); + + G_LOCK (requests); + + while (g_hash_table_lookup (requests, id) != NULL) + { + r = g_random_int (); + g_free (id); + id = g_strdup_printf ("/org/freedesktop/portal/desktop/request/%s/%s/%u", sender, token, r); + } + + request->id = id; + g_hash_table_insert (requests, id, request); + + G_UNLOCK (requests); + + g_dbus_interface_skeleton_set_flags (G_DBUS_INTERFACE_SKELETON (request), + G_DBUS_INTERFACE_SKELETON_FLAGS_HANDLE_METHOD_INVOCATIONS_IN_THREAD); + g_signal_connect (request, "g-authorize-method", + G_CALLBACK (request_authorize_callback), + request->sender); + + + g_object_set_data_full (G_OBJECT (invocation), "request", request, g_object_unref); +} + +Request * +request_from_invocation (GDBusMethodInvocation *invocation) +{ + return g_object_get_data (G_OBJECT (invocation), "request"); +} + +void +request_export (Request *request, + GDBusConnection *connection) +{ + g_autoptr(GError) error = NULL; + + if (!g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (request), + connection, + request->id, + &error)) + { + g_warning ("Error exporting request: %s", error->message); + g_clear_error (&error); + } + + g_object_ref (request); + request->exported = TRUE; +} + +void +request_unexport (Request *request) +{ + int fd; + + fd = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (request), "fd")); + if (fd != -1) + close (fd); + + request->exported = FALSE; + g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (request)); + g_object_unref (request); +} + +void +request_set_impl_request (Request *request, + XdpDbusImplRequest *impl_request) +{ + g_set_object (&request->impl_request, impl_request); +} + +void +close_requests_in_thread_func (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + const char *sender = (const char *)task_data; + GSList *list = NULL; + GSList *l; + GHashTableIter iter; + Request *request; + + G_LOCK (requests); + if (requests) + { + g_hash_table_iter_init (&iter, requests); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&request)) + { + if (strcmp (sender, request->sender) == 0) + list = g_slist_prepend (list, g_object_ref (request)); + } + } + G_UNLOCK (requests); + + for (l = list; l; l = l->next) + { + Request *request = l->data; + + REQUEST_AUTOLOCK (request); + + if (request->exported) + { + if (request->impl_request) + xdp_dbus_impl_request_call_close_sync (request->impl_request, NULL, NULL); + + request_unexport (request); + } + } + + g_slist_free_full (list, g_object_unref); +} + +void +close_requests_for_sender (const char *sender) +{ + GTask *task; + + task = g_task_new (NULL, NULL, NULL, NULL); + g_task_set_task_data (task, g_strdup (sender), g_free); + g_task_run_in_thread (task, close_requests_in_thread_func); + g_object_unref (task); +} + diff --git a/src/request.h b/src/request.h new file mode 100644 index 0000000..515a7d7 --- /dev/null +++ b/src/request.h @@ -0,0 +1,86 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Alexander Larsson + * Matthias Clasen + */ + +#pragma once + +#include "xdp-utils.h" +#include "xdp-dbus.h" +#include "xdp-impl-dbus.h" + +typedef enum { + XDG_DESKTOP_PORTAL_RESPONSE_SUCCESS = 0, + XDG_DESKTOP_PORTAL_RESPONSE_CANCELLED, + XDG_DESKTOP_PORTAL_RESPONSE_OTHER +} XdgDesktopPortalResponseEnum; + +typedef struct _Request Request; +typedef struct _RequestClass RequestClass; + +struct _Request +{ + XdpDbusRequestSkeleton parent_instance; + + gboolean exported; + char *id; + char *sender; + GMutex mutex; + XdpAppInfo *app_info; + + XdpDbusImplRequest *impl_request; +}; + +struct _RequestClass +{ + XdpDbusRequestSkeletonClass parent_class; +}; + +GType request_get_type (void) G_GNUC_CONST; + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (Request, g_object_unref) + +void request_init_invocation (GDBusMethodInvocation *invocation, XdpAppInfo *app_info); +Request *request_from_invocation (GDBusMethodInvocation *invocation); +void request_export (Request *request, + GDBusConnection *connection); +void request_unexport (Request *request); +void close_requests_for_sender (const char *sender); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (XdpDbusImplRequest, g_object_unref) + +void request_set_impl_request (Request *request, + XdpDbusImplRequest *impl_request); + +static inline void +auto_unlock_helper (GMutex **mutex) +{ + if (*mutex) + g_mutex_unlock (*mutex); +} + +static inline GMutex * +auto_lock_helper (GMutex *mutex) +{ + if (mutex) + g_mutex_lock (mutex); + return mutex; +} + +#define REQUEST_AUTOLOCK(request) G_GNUC_UNUSED __attribute__((cleanup (auto_unlock_helper))) GMutex * G_PASTE (request_auto_unlock, __LINE__) = auto_lock_helper (&request->mutex); diff --git a/src/restore-token.c b/src/restore-token.c new file mode 100644 index 0000000..2e6f31f --- /dev/null +++ b/src/restore-token.c @@ -0,0 +1,378 @@ +/* + * Copyright 2021-2022 Endless OS Foundation, LLC + * Copyright 2023 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + */ + +#include "config.h" + +#include "permissions.h" +#include "restore-token.h" + +static GMutex transient_permissions_lock; +static GHashTable *transient_permissions; + +#define RESTORE_DATA_TYPE "(suv)" + +static void +internal_closed_cb (Session *session) +{ + g_autoptr(GMutexLocker) locker = NULL; + GHashTableIter iter; + const char *key; + + locker = g_mutex_locker_new (&transient_permissions_lock); + + if (!transient_permissions) + return; + + g_hash_table_iter_init (&iter, transient_permissions); + while (g_hash_table_iter_next (&iter, (gpointer *) &key, NULL)) + { + g_auto(GStrv) split = g_strsplit (key, "/", 2); + + if (split && split[0] && g_strcmp0 (split[0], session->sender) == 0) + g_hash_table_iter_remove (&iter); + } +} + +void +xdp_session_persistence_set_transient_permissions (Session *session, + const char *restore_token, + GVariant *restore_data) +{ + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&transient_permissions_lock); + + if (!transient_permissions) + { + transient_permissions = + g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, (GDestroyNotify) g_variant_unref); + } + + g_hash_table_insert (transient_permissions, + g_strdup_printf ("%s/%s", session->sender, restore_token), + g_variant_ref (restore_data)); + + if (!session->persistence.has_transient_permissions) + { + g_signal_connect (session, "internal-closed", + G_CALLBACK (internal_closed_cb), NULL); + session->persistence.has_transient_permissions = TRUE; + } +} + +void +xdp_session_persistence_delete_transient_permissions (Session *session, + const char *restore_token) +{ + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&transient_permissions_lock); + g_autofree char *id = NULL; + + if (!transient_permissions) + return; + + id = g_strdup_printf ("%s/%s", session->sender, restore_token); + g_hash_table_remove (transient_permissions, id); +} + +GVariant * +xdp_session_persistence_get_transient_permissions (Session *session, + const char *restore_token) +{ + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&transient_permissions_lock); + g_autofree char *id = NULL; + GVariant *permissions; + + if (!transient_permissions) + return NULL; + + id = g_strdup_printf ("%s/%s", session->sender, restore_token); + permissions = g_hash_table_lookup (transient_permissions, id); + return permissions ? g_variant_ref (permissions) : NULL; +} + +void +xdp_session_persistence_set_persistent_permissions (Session *session, + const char *table, + const char *restore_token, + GVariant *restore_data) +{ + g_autoptr(GError) error = NULL; + + set_permission_sync (session->app_id, table, restore_token, PERMISSION_YES); + + if (!xdp_dbus_impl_permission_store_call_set_value_sync (get_permission_store (), + table, + TRUE, + restore_token, + g_variant_new_variant (restore_data), + NULL, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("Error setting permission store value: %s", error->message); + } +} + +void +xdp_session_persistence_delete_persistent_permissions (Session *session, + const char *table, + const char *restore_token) +{ + + g_autoptr(GError) error = NULL; + + if (!xdp_dbus_impl_permission_store_call_delete_sync (get_permission_store (), + table, + restore_token, + NULL, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("Error deleting permission: %s", error->message); + } +} + +GVariant * +xdp_session_persistence_get_persistent_permissions (Session *session, + const char *table, + const char *restore_token) +{ + g_autoptr(GVariant) perms = NULL; + g_autoptr(GVariant) data = NULL; + g_autoptr(GError) error = NULL; + const char **permissions; + + if (!xdp_dbus_impl_permission_store_call_lookup_sync (get_permission_store (), + table, + restore_token, + &perms, + &data, + NULL, + &error)) + { + return NULL; + } + + if (!perms || !g_variant_lookup (perms, session->app_id, "^a&s", &permissions)) + return NULL; + + if (!data) + return NULL; + + return g_variant_get_child_value (data, 0); +} + +void +xdp_session_persistence_replace_restore_token_with_data (Session *session, + const char *table, + GVariant **in_out_options, + char **out_restore_token) +{ + GVariantIter options_iter; + GVariantBuilder options_builder; + char *key; + GVariant *value; + + g_variant_iter_init (&options_iter, *in_out_options); + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + + while (g_variant_iter_next (&options_iter, "{sv}", &key, &value)) + { + if (g_strcmp0 (key, "restore_token") == 0) + { + g_autofree char *restore_token = NULL; + g_autoptr(GVariant) restore_data = NULL; + + restore_token = g_variant_dup_string (value, NULL); + + /* Lookup permissions in memory first, and fallback to the permission + * store if not found. Immediately delete them now as a safety measure, + * since they'll be stored again when the session is closed. + * + * Notice that transient mode uses the sender name, whereas persistent + * mode uses the app id. + */ + restore_data = + xdp_session_persistence_get_transient_permissions (session, + restore_token); + if (restore_data) + { + xdp_session_persistence_delete_transient_permissions (session, + restore_token); + } + else + { + restore_data = + xdp_session_persistence_get_persistent_permissions (session, + table, + restore_token); + if (restore_data) + { + xdp_session_persistence_delete_persistent_permissions (session, + table, + restore_token); + } + } + + if (restore_data && + g_variant_check_format_string (restore_data, RESTORE_DATA_TYPE, FALSE)) + { + g_debug ("Replacing 'restore_token' with portal-specific data"); + g_variant_builder_add (&options_builder, "{sv}", + "restore_data", restore_data); + *out_restore_token = g_steal_pointer (&restore_token); + } + } + else + { + g_variant_builder_add (&options_builder, "{sv}", + key, g_variant_ref (value)); + } + + g_free (key); + g_variant_unref (value); + } + + *in_out_options = g_variant_builder_end (&options_builder); +} + +void +xdp_session_persistence_generate_and_save_restore_token (Session *session, + const char *table, + PersistMode persist_mode, + char **in_out_restore_token, + GVariant **in_out_restore_data) +{ + if (!*in_out_restore_data) + { + if (*in_out_restore_token) + { + xdp_session_persistence_delete_persistent_permissions (session, + table, + *in_out_restore_token); + xdp_session_persistence_delete_transient_permissions (session, + *in_out_restore_token); + } + + g_clear_pointer (in_out_restore_token, g_free); + return; + } + + switch (persist_mode) + { + case PERSIST_MODE_NONE: + if (*in_out_restore_token) + { + xdp_session_persistence_delete_persistent_permissions (session, + table, + *in_out_restore_token); + xdp_session_persistence_delete_transient_permissions (session, + *in_out_restore_token); + } + + g_clear_pointer (in_out_restore_token, g_free); + g_clear_pointer (in_out_restore_data, g_variant_unref); + break; + + case PERSIST_MODE_TRANSIENT: + if (!*in_out_restore_token) + *in_out_restore_token = g_uuid_string_random (); + + xdp_session_persistence_set_transient_permissions (session, + *in_out_restore_token, + *in_out_restore_data); + break; + + case PERSIST_MODE_PERSISTENT: + if (!*in_out_restore_token) + *in_out_restore_token = g_uuid_string_random (); + + xdp_session_persistence_set_persistent_permissions (session, + table, + *in_out_restore_token, + *in_out_restore_data); + + break; + } +} + +void +xdp_session_persistence_replace_restore_data_with_token (Session *session, + const char *table, + GVariant **in_out_results, + PersistMode *in_out_persist_mode, + char **in_out_restore_token, + GVariant **in_out_restore_data) +{ + g_autoptr(GVariant) results = *in_out_results; + GVariantBuilder results_builder; + GVariantIter iter; + const char *key; + GVariant *value; + gboolean found_restore_data = FALSE; + + g_variant_builder_init (&results_builder, G_VARIANT_TYPE_VARDICT); + + g_variant_iter_init (&iter, results); + while (g_variant_iter_next (&iter, "{&sv}", &key, &value)) + { + if (g_strcmp0 (key, "restore_data") == 0) + { + if (g_variant_check_format_string (value, RESTORE_DATA_TYPE, FALSE)) + { + *in_out_restore_data = g_variant_ref_sink (value); + found_restore_data = TRUE; + } + else + { + g_warning ("Received restore data in invalid variant format ('%s'; expected '%s')", + g_variant_get_type_string (value), + RESTORE_DATA_TYPE); + } + } + else if (g_strcmp0 (key, "persist_mode") == 0) + { + *in_out_persist_mode = MIN (*in_out_persist_mode, + g_variant_get_uint32 (value)); + } + else + { + g_variant_builder_add (&results_builder, "{sv}", key, value); + } + } + + if (found_restore_data) + { + g_debug ("Replacing restore data received from portal impl with a token"); + + xdp_session_persistence_generate_and_save_restore_token (session, + table, + *in_out_persist_mode, + in_out_restore_token, + in_out_restore_data); + g_variant_builder_add (&results_builder, "{sv}", "restore_token", + g_variant_new_string (*in_out_restore_token)); + } + else + { + *in_out_persist_mode = PERSIST_MODE_NONE; + } + + *in_out_results = g_variant_builder_end (&results_builder); +} diff --git a/src/restore-token.h b/src/restore-token.h new file mode 100644 index 0000000..1853017 --- /dev/null +++ b/src/restore-token.h @@ -0,0 +1,69 @@ +/* + * Copyright © 2023 Red Hat + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + */ + +#pragma once + +#include "session.h" + +typedef enum _PersistMode +{ + PERSIST_MODE_NONE = 0, + PERSIST_MODE_TRANSIENT = 1, + PERSIST_MODE_PERSISTENT = 2, +} PersistMode; + +void xdp_session_persistence_set_transient_permissions (Session *session, + const char *restore_token, + GVariant *restore_data); + +void xdp_session_persistence_delete_transient_permissions (Session *session, + const char *restore_token); + +GVariant * xdp_session_persistence_get_transient_permissions (Session *session, + const char *restore_token); + +void xdp_session_persistence_set_persistent_permissions (Session *session, + const char *table, + const char *restore_token, + GVariant *restore_data); + +void xdp_session_persistence_delete_persistent_permissions (Session *session, + const char *table, + const char *restore_token); + +GVariant * xdp_session_persistence_get_persistent_permissions (Session *session, + const char *table, + const char *restore_token); + +void xdp_session_persistence_replace_restore_token_with_data (Session *session, + const char *table, + GVariant **in_out_options, + char **out_restore_token); + +void xdp_session_persistence_replace_restore_data_with_token (Session *session, + const char *table, + GVariant **in_out_results, + PersistMode *in_out_persist_mode, + char **in_out_restore_token, + GVariant **in_out_restore_data); + +void xdp_session_persistence_generate_and_save_restore_token (Session *session, + const char *table, + PersistMode persist_mode, + char **in_out_restore_token, + GVariant **in_out_restore_data); diff --git a/src/rewrite-launchers.c b/src/rewrite-launchers.c new file mode 100644 index 0000000..1d6d7c9 --- /dev/null +++ b/src/rewrite-launchers.c @@ -0,0 +1,281 @@ +/* + * Copyright © 2022 Matthew Leeds + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthew Leeds + */ + +#include "config.h" + +#include +#include + +#include "dynamic-launcher.h" +#include "xdp-utils.h" + +static char * +find_renamed_app_id (const char *old_app_id) +{ + g_autofree char *renamed_to = NULL; + g_autofree char *desktop_id = NULL; + + desktop_id = g_strconcat (old_app_id, ".desktop", NULL); + + GList *app_infos = g_app_info_get_all (); + for (GList *l = app_infos; l; l = l->next) + { + GDesktopAppInfo *info = l->data; + g_auto(GStrv) renamed_from = NULL; + renamed_from = g_desktop_app_info_get_string_list (info, "X-Flatpak-RenamedFrom", NULL); + if (renamed_from == NULL) + continue; + if (!g_strv_contains ((const char * const *)renamed_from, desktop_id)) + continue; + + renamed_to = g_desktop_app_info_get_string (info, "X-Flatpak"); + break; + } + + g_list_free_full (app_infos, g_object_unref); + return g_steal_pointer (&renamed_to); +} + +/* + * It's possible an app was renamed using Flatpak's end-of-life-rebase + * mechanism, and either (a) the app was installed system-wide and the update + * was applied by another user, so the migration for this user has to happen + * when this binary runs at the start of the session, or (b) the version of + * Flatpak is not new enough for the migration of the launchers to be handled + * by Flatpak, so x-d-p has to do it. + * + * This function also handles deleting the launchers in case the parent app has + * been uninstalled. + */ +static gboolean +migrate_renamed_app_launchers (void) +{ + g_autoptr(GFile) desktop_dir = NULL; + g_autoptr(GFileEnumerator) children = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *icon_dir_path = NULL; + g_autofree char *desktop_dir_path = NULL; + gboolean success = TRUE; + + desktop_dir_path = g_build_filename (g_get_user_data_dir (), XDG_PORTAL_APPLICATIONS_DIR, NULL); + desktop_dir = g_file_new_for_path (desktop_dir_path); + children = g_file_enumerate_children (desktop_dir, "standard::name", G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, &error); + if (children == NULL) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + { + g_warning ("Error encountered enumerating launchers: %s", error->message); + success = FALSE; + } + return success; + } + + icon_dir_path = g_build_filename (g_get_user_data_dir (), XDG_PORTAL_ICONS_DIR, NULL); + for (;;) + { + g_autoptr(GFileInfo) info = g_file_enumerator_next_file (children, NULL, NULL); + const char *desktop_name; + g_autofree char *desktop_path = NULL; + g_autofree char *icon_path = NULL; + g_autofree char *tryexec_path = NULL; + g_autofree char *app_id = NULL; + g_autofree char *renamed_to = NULL; + g_autoptr(GKeyFile) key_file = NULL; + g_autoptr(GFile) icon_file = NULL; + g_autoptr(GFile) desktop_file = NULL; + + if (info == NULL) + return success; + + desktop_name = g_file_info_get_name (info); + if (!g_str_has_suffix (desktop_name, ".desktop")) + continue; + + desktop_path = g_build_filename (g_file_peek_path (desktop_dir), desktop_name, NULL); + key_file = g_key_file_new (); + if (!g_key_file_load_from_file (key_file, desktop_path, + G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS, + &error)) + { + g_warning ("Error encountered loading key file %s: %s", desktop_path, error->message); + g_clear_error (&error); + success = FALSE; + continue; + } + + tryexec_path = g_key_file_get_string (key_file, G_KEY_FILE_DESKTOP_GROUP, "TryExec", NULL); + if (tryexec_path == NULL) + continue; + if (!g_path_is_absolute (tryexec_path)) + { + /* Here we're just checking for existence not a renamed binary */ + renamed_to = g_find_program_in_path (tryexec_path); + if (renamed_to) + continue; + } + else if (g_file_test (tryexec_path, G_FILE_TEST_IS_EXECUTABLE)) + { + continue; + } + else if (g_key_file_has_key (key_file, G_KEY_FILE_DESKTOP_GROUP, "X-Flatpak", NULL)) + { + app_id = g_path_get_basename (tryexec_path); + if (strlen(app_id) < 2 || + !g_dbus_is_name (app_id) || + !g_str_has_prefix (desktop_name, app_id)) + { + g_warning ("Unable to determine app id for %s", desktop_name); + success = FALSE; + continue; + } + renamed_to = find_renamed_app_id (app_id); + } + + icon_path = g_key_file_get_string (key_file, G_KEY_FILE_DESKTOP_GROUP, "Icon", NULL); + if (g_str_has_prefix (icon_path, icon_dir_path)) + icon_file = g_file_new_for_path (icon_path); + + desktop_file = g_file_new_for_path (desktop_path); + + if (renamed_to == NULL) + { + g_autoptr(GFile) link_file = NULL; + + link_file = g_file_new_build_filename (g_get_user_data_dir (), "applications", desktop_name, NULL); + if (!g_file_delete (link_file, NULL, &error)) + { + g_warning ("Couldn't delete sym link %s: %s", g_file_peek_path (link_file), error->message); + g_clear_error (&error); + success = FALSE; + } + + if (!g_file_delete (desktop_file, NULL, &error)) + { + g_warning ("Couldn't delete desktop file %s: %s", g_file_peek_path (desktop_file), error->message); + g_clear_error (&error); + success = FALSE; + } + + if (icon_file && !g_file_delete (icon_file, NULL, &error)) + { + g_warning ("Couldn't delete icon file %s: %s", g_file_peek_path (icon_file), error->message); + g_clear_error (&error); + success = FALSE; + } + } + else /* renamed_to != NULL */ + { + g_autoptr(GFile) link_file = NULL; + g_autoptr(GFile) new_link_file = NULL; + g_autoptr(GString) data_string = NULL; + g_autoptr(GKeyFile) new_key_file = NULL; + g_autofree char *new_desktop = NULL; + g_autofree char *new_desktop_path = NULL; + g_autofree char *new_icon = NULL; + g_autofree char *icon_basename = NULL; + g_autofree char *link_path = NULL; + g_autofree char *relative_path = NULL; + g_autofree char *old_data = NULL; + const gchar *desktop_suffix; + gchar *icon_suffix; + + if (!g_key_file_has_key (key_file, G_KEY_FILE_DESKTOP_GROUP, "X-Flatpak", NULL)) + g_assert_not_reached (); + + /* Fix paths in desktop file with a find-and-replace. */ + old_data = g_key_file_to_data (key_file, NULL, NULL); + data_string = g_string_new ((const char *)old_data); + g_string_replace (data_string, app_id, renamed_to, 0); + new_key_file = g_key_file_new (); + if (!g_key_file_load_from_data (new_key_file, data_string->str, -1, + G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS, + &error)) + { + g_warning ("Cannot load desktop file %s after rewrite: %s", desktop_path, error->message); + g_warning ("Key file contents:\n%s\n", (const char *)data_string->str); + g_clear_error (&error); + success = FALSE; + continue; + } + + /* Write it out at the new path */ + g_assert (g_str_has_prefix (desktop_name, app_id)); + desktop_suffix = desktop_name + strlen (app_id); + new_desktop = g_strconcat (renamed_to, desktop_suffix, NULL); + new_desktop_path = g_build_filename (desktop_dir_path, new_desktop, NULL); + if (!g_key_file_save_to_file (new_key_file, new_desktop_path, &error)) + { + g_warning ("Couldn't rewrite desktop file %s to %s: %s", + desktop_path, new_desktop_path, error->message); + g_clear_error (&error); + success = FALSE; + continue; + } + + /* Fix symlink */ + link_path = g_build_filename (g_get_user_data_dir (), "applications", desktop_name, NULL); + link_file = g_file_new_for_path (link_path); + relative_path = g_build_filename ("..", XDG_PORTAL_APPLICATIONS_DIR, new_desktop, NULL); + g_file_delete (link_file, NULL, NULL); + new_link_file = g_file_new_build_filename (g_get_user_data_dir (), "applications", new_desktop, NULL); + if (!g_file_make_symbolic_link (new_link_file, relative_path, NULL, &error)) + { + g_warning ("Unable to rename desktop file link %s -> %s: %s", + desktop_name, new_desktop, error->message); + g_clear_error (&error); + success = FALSE; + continue; + } + + /* Delete the old desktop file */ + unlink (desktop_path); + + /* And rename the icon */ + if (icon_file) + { + icon_basename = g_path_get_basename (icon_path); + if (!g_str_has_prefix (icon_basename, app_id)) + continue; + + icon_suffix = icon_basename + strlen (app_id); + new_icon = g_strconcat (renamed_to, icon_suffix, NULL); + if (!g_file_set_display_name (icon_file, new_icon, NULL, &error)) + { + g_warning ("Unable to rename icon file %s -> %s: %s", + icon_basename, new_icon, error->message); + g_clear_error (&error); + success = FALSE; + continue; + } + } + } + } + + return success; +} + +int +main (int argc, char *argv[]) +{ + if (!migrate_renamed_app_launchers ()) + return 1; + + return 0; +} diff --git a/src/screen-cast.c b/src/screen-cast.c new file mode 100644 index 0000000..77d1df2 --- /dev/null +++ b/src/screen-cast.c @@ -0,0 +1,1165 @@ +/* + * Copyright © 2017-2018 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + */ + +#include "config.h" + +#include +#include +#include + +#include "session.h" +#include "screen-cast.h" +#include "remote-desktop.h" +#include "request.h" +#include "restore-token.h" +#include "permissions.h" +#include "pipewire.h" +#include "xdp-dbus.h" +#include "xdp-impl-dbus.h" +#include "xdp-utils.h" + +#define PERMISSION_ITEM(item_id, item_permissions) \ + ((struct pw_permission) { \ + .id = item_id, \ + .permissions = item_permissions \ + }) +#define SCREEN_CAST_TABLE "screencast" + +typedef struct _ScreenCast ScreenCast; +typedef struct _ScreenCastClass ScreenCastClass; + +struct _ScreenCast +{ + XdpDbusScreenCastSkeleton parent_instance; +}; + +struct _ScreenCastClass +{ + XdpDbusScreenCastSkeletonClass parent_class; +}; + +static XdpDbusImplScreenCast *impl; +static int impl_version; +static ScreenCast *screen_cast; + +static unsigned int available_cursor_modes = 0; + +GType screen_cast_get_type (void); +static void screen_cast_iface_init (XdpDbusScreenCastIface *iface); + +static GQuark quark_request_session; + +struct _ScreenCastStream +{ + uint32_t id; + int32_t width; + int32_t height; +}; + +G_DEFINE_TYPE_WITH_CODE (ScreenCast, screen_cast, + XDP_DBUS_TYPE_SCREEN_CAST_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_SCREEN_CAST, + screen_cast_iface_init)) + +typedef enum _ScreenCastSessionState +{ + SCREEN_CAST_SESSION_STATE_INIT, + SCREEN_CAST_SESSION_STATE_SELECTING_SOURCES, + SCREEN_CAST_SESSION_STATE_SOURCES_SELECTED, + SCREEN_CAST_SESSION_STATE_STARTING, + SCREEN_CAST_SESSION_STATE_STARTED, + SCREEN_CAST_SESSION_STATE_CLOSED +} ScreenCastSessionState; + +typedef struct _ScreenCastSession +{ + Session parent; + + ScreenCastSessionState state; + + GList *streams; + char *restore_token; + PersistMode persist_mode; + GVariant *restore_data; +} ScreenCastSession; + +typedef struct _ScreenCastSessionClass +{ + SessionClass parent_class; +} ScreenCastSessionClass; + +GType screen_cast_session_get_type (void); + +G_DEFINE_TYPE (ScreenCastSession, screen_cast_session, session_get_type ()) + + +static gboolean +is_screen_cast_session (Session *session) +{ + return G_TYPE_CHECK_INSTANCE_TYPE (session, screen_cast_session_get_type ()); +} + +static ScreenCastSession * +screen_cast_session_new (GVariant *options, + Request *request, + GError **error) +{ + Session *session; + GDBusInterfaceSkeleton *interface_skeleton = + G_DBUS_INTERFACE_SKELETON (request); + const char *session_token; + GDBusConnection *connection = + g_dbus_interface_skeleton_get_connection (interface_skeleton); + GDBusConnection *impl_connection = + g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)); + const char *impl_dbus_name = g_dbus_proxy_get_name (G_DBUS_PROXY (impl)); + + session_token = lookup_session_token (options); + session = g_initable_new (screen_cast_session_get_type (), NULL, error, + "sender", request->sender, + "app-id", xdp_app_info_get_id (request->app_info), + "token", session_token, + "connection", connection, + "impl-connection", impl_connection, + "impl-dbus-name", impl_dbus_name, + NULL); + + if (session) + g_debug ("screen cast session owned by '%s' created", session->sender); + + return (ScreenCastSession*)session; +} + +static void +create_session_done (GObject *source_object, + GAsyncResult *res, + gpointer data) +{ + g_autoptr(Request) request = data; + Session *session; + guint response = 2; + gboolean should_close_session; + GVariantBuilder results_builder; + g_autoptr(GError) error = NULL; + + REQUEST_AUTOLOCK (request); + + session = g_object_get_qdata (G_OBJECT (request), quark_request_session); + SESSION_AUTOLOCK_UNREF (g_object_ref (session)); + g_object_set_qdata (G_OBJECT (request), quark_request_session, NULL); + + g_variant_builder_init (&results_builder, G_VARIANT_TYPE_VARDICT); + + if (!xdp_dbus_impl_screen_cast_call_create_session_finish (impl, + &response, + NULL, + res, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("A backend call failed: %s", error->message); + should_close_session = TRUE; + goto out; + } + + if (request->exported && response == 0) + { + if (!session_export (session, &error)) + { + g_warning ("Failed to export session: %s", error->message); + response = 2; + should_close_session = TRUE; + goto out; + } + + should_close_session = FALSE; + session_register (session); + } + else + { + should_close_session = TRUE; + } + + g_variant_builder_add (&results_builder, "{sv}", + "session_handle", g_variant_new ("s", session->id)); + +out: + if (request->exported) + { + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + response, + g_variant_builder_end (&results_builder)); + request_unexport (request); + } + else + { + g_variant_builder_clear (&results_builder); + } + + if (should_close_session) + session_close (session, FALSE); +} + +static gboolean +handle_create_session (XdpDbusScreenCast *object, + GDBusMethodInvocation *invocation, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + g_autoptr(GError) error = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + Session *session; + GVariantBuilder options_builder; + GVariant *options; + + REQUEST_AUTOLOCK (request); + + impl_request = + xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, &error); + if (!impl_request) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request (request, impl_request); + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + session = (Session *)screen_cast_session_new (arg_options, request, &error); + if (!session) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + options = g_variant_builder_end (&options_builder); + + g_object_set_qdata_full (G_OBJECT (request), + quark_request_session, + g_object_ref (session), + g_object_unref); + + xdp_dbus_impl_screen_cast_call_create_session (impl, + request->id, + session->id, + xdp_app_info_get_id (request->app_info), + options, + NULL, + create_session_done, + g_object_ref (request)); + + xdp_dbus_screen_cast_complete_create_session (object, invocation, request->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +select_sources_done (GObject *source_object, + GAsyncResult *res, + gpointer data) +{ + g_autoptr(Request) request = data; + Session *session; + guint response = 2; + gboolean should_close_session; + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) results = NULL; + + REQUEST_AUTOLOCK (request); + + session = g_object_get_qdata (G_OBJECT (request), quark_request_session); + SESSION_AUTOLOCK_UNREF (g_object_ref (session)); + g_object_set_qdata (G_OBJECT (request), quark_request_session, NULL); + + if (!xdp_dbus_impl_screen_cast_call_select_sources_finish (impl, + &response, + &results, + res, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("A backend call failed: %s", error->message); + } + + should_close_session = !request->exported || response != 0; + + if (request->exported) + { + if (!results) + { + GVariantBuilder results_builder; + + g_variant_builder_init (&results_builder, G_VARIANT_TYPE_VARDICT); + results = g_variant_ref_sink (g_variant_builder_end (&results_builder)); + } + + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), response, results); + request_unexport (request); + } + + if (should_close_session) + { + session_close (session, TRUE); + } + else if (!session->closed) + { + if (is_screen_cast_session (session)) + { + ScreenCastSession *screen_cast_session = (ScreenCastSession *)session; + + g_assert_cmpint (screen_cast_session->state, + ==, + SCREEN_CAST_SESSION_STATE_SELECTING_SOURCES); + screen_cast_session->state = SCREEN_CAST_SESSION_STATE_SOURCES_SELECTED; + } + else if (is_remote_desktop_session (session)) + { + RemoteDesktopSession *remote_desktop_session = + (RemoteDesktopSession *)session; + + remote_desktop_session_sources_selected (remote_desktop_session); + } + } +} + +static gboolean +validate_source_types (const char *key, + GVariant *value, + GVariant *options, + GError **error) +{ + guint32 types = g_variant_get_uint32 (value); + + if ((types & ~(1 | 2 | 4)) != 0) + { + g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Unsupported device type: %x", types & ~(1 | 2 | 4)); + return FALSE; + } + + return TRUE; +} + +static gboolean +validate_cursor_mode (const char *key, + GVariant *value, + GVariant *options, + GError **error) +{ + uint32_t mode = g_variant_get_uint32 (value); + + if (__builtin_popcount (mode) != 1) + { + g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Invalid cursor mode %x", mode); + return FALSE; + } + + if (!(available_cursor_modes & mode)) + { + g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Unavailable cursor mode %x", mode); + return FALSE; + } + + return TRUE; +} + +static gboolean +validate_restore_token (const char *key, + GVariant *value, + GVariant *options, + GError **error) +{ + const char *restore_token = g_variant_get_string (value, NULL); + + if (!g_uuid_string_is_valid (restore_token)) + { + g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Restore token is not a valid UUID string"); + return FALSE; + } + + return TRUE; +} + +static gboolean +validate_persist_mode (const char *key, + GVariant *value, + GVariant *options, + GError **error) +{ + uint32_t mode = g_variant_get_uint32 (value); + + if (mode > PERSIST_MODE_PERSISTENT) + { + g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Invalid persist mode %x", mode); + return FALSE; + } + + return TRUE; +} + +static XdpOptionKey screen_cast_select_sources_options[] = { + { "types", G_VARIANT_TYPE_UINT32, validate_source_types }, + { "multiple", G_VARIANT_TYPE_BOOLEAN, NULL }, + { "cursor_mode", G_VARIANT_TYPE_UINT32, validate_cursor_mode }, + { "restore_token", G_VARIANT_TYPE_STRING, validate_restore_token }, + { "persist_mode", G_VARIANT_TYPE_UINT32, validate_persist_mode }, +}; + +static gboolean +replace_screen_cast_restore_token_with_data (Session *session, + GVariant **in_out_options, + GError **error) +{ + g_autoptr(GVariant) options = NULL; + PersistMode persist_mode; + + options = *in_out_options; + + if (!g_variant_lookup (options, "persist_mode", "u", &persist_mode)) + persist_mode = PERSIST_MODE_NONE; + + if (is_remote_desktop_session (session)) + { + if (persist_mode != PERSIST_MODE_NONE || + xdp_variant_contains_key (options, "restore_token")) + { + g_set_error (error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Remote desktop sessions cannot persist"); + return FALSE; + } + } + + if (is_screen_cast_session (session)) + { + ScreenCastSession *screen_cast_session = (ScreenCastSession *)session; + + screen_cast_session->persist_mode = persist_mode; + xdp_session_persistence_replace_restore_token_with_data (session, + SCREEN_CAST_TABLE, + in_out_options, + &screen_cast_session->restore_token); + } + else + { + *in_out_options = g_steal_pointer (&options); + } + + return TRUE; +} + +static gboolean +handle_select_sources (XdpDbusScreenCast *object, + GDBusMethodInvocation *invocation, + const char *arg_session_handle, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + Session *session; + g_autoptr(GError) error = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + GVariantBuilder options_builder; + GVariant *options; + + REQUEST_AUTOLOCK (request); + + session = acquire_session (arg_session_handle, request); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + if (is_screen_cast_session (session)) + { + ScreenCastSession *screen_cast_session = (ScreenCastSession *)session; + + switch (screen_cast_session->state) + { + case SCREEN_CAST_SESSION_STATE_INIT: + break; + case SCREEN_CAST_SESSION_STATE_SELECTING_SOURCES: + case SCREEN_CAST_SESSION_STATE_SOURCES_SELECTED: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Sources already selected"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + case SCREEN_CAST_SESSION_STATE_STARTING: + case SCREEN_CAST_SESSION_STATE_STARTED: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Can only select sources before starting"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + case SCREEN_CAST_SESSION_STATE_CLOSED: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + } + else if (is_remote_desktop_session (session)) + { + RemoteDesktopSession *remote_desktop_session = + (RemoteDesktopSession *)session; + + if (!remote_desktop_session_can_select_sources (remote_desktop_session)) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid state"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + } + else + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + impl_request = + xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, &error); + if (!impl_request) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request (request, impl_request); + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &options_builder, + screen_cast_select_sources_options, + G_N_ELEMENTS (screen_cast_select_sources_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + options = g_variant_builder_end (&options_builder); + + /* If 'restore_token' is passed, lookup the corresponding data in the + * permission store and / or the GHashTable with transient permissions. + * Portal implementations do not have access to the restore token. + */ + if (!replace_screen_cast_restore_token_with_data (session, &options, &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_object_set_qdata_full (G_OBJECT (request), + quark_request_session, + g_object_ref (session), + g_object_unref); + if (is_screen_cast_session (session)) + { + ((ScreenCastSession *)session)->state = + SCREEN_CAST_SESSION_STATE_SELECTING_SOURCES; + } + + xdp_dbus_impl_screen_cast_call_select_sources (impl, + request->id, + arg_session_handle, + xdp_app_info_get_id (request->app_info), + options, + NULL, + select_sources_done, + g_object_ref (request)); + + xdp_dbus_screen_cast_complete_select_sources (object, invocation, request->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +uint32_t +screen_cast_stream_get_pipewire_node_id (ScreenCastStream *stream) +{ + return stream->id; +} + +static void +append_stream_permissions (PipeWireRemote *remote, + GArray *permission_items, + GList *streams) +{ + GList *l; + + for (l = streams; l; l = l->next) + { + ScreenCastStream *stream = l->data; + uint32_t stream_id; + + stream_id = screen_cast_stream_get_pipewire_node_id (stream); + g_array_append_val (permission_items, + PERMISSION_ITEM (stream_id, PW_PERM_RWX)); + } +} + +static PipeWireRemote * +open_pipewire_screen_cast_remote (const char *app_id, + GList *streams, + GError **error) +{ + struct pw_properties *pipewire_properties; + PipeWireRemote *remote; + g_autoptr(GArray) permission_items = NULL; + + pipewire_properties = pw_properties_new ("pipewire.access.portal.app_id", app_id, + "pipewire.access.portal.media_roles", "", + NULL); + remote = pipewire_remote_new_sync (pipewire_properties, + NULL, NULL, NULL, NULL, + error); + if (!remote) + return FALSE; + + permission_items = g_array_new (FALSE, TRUE, sizeof (struct pw_permission)); + + /* + * PipeWire:Interface:Core + * Needs rwx to be able create the sink node using the create-object method + */ + g_array_append_val (permission_items, + PERMISSION_ITEM (PW_ID_CORE, PW_PERM_RWX)); + + /* + * PipeWire:Interface:NodeFactory + * Needs r-- so it can be passed to create-object when creating the sink node. + */ + g_array_append_val (permission_items, + PERMISSION_ITEM (remote->node_factory_id, PW_PERM_R)); + + append_stream_permissions (remote, permission_items, streams); + + /* + * Hide all existing and future nodes (except the ones we explicitly list above). + */ + g_array_append_val (permission_items, + PERMISSION_ITEM (PW_ID_ANY, 0)); + + pw_client_update_permissions (pw_core_get_client(remote->core), + permission_items->len, + (const struct pw_permission *)permission_items->data); + + pipewire_remote_roundtrip (remote); + + return remote; +} + +void +screen_cast_stream_get_size (ScreenCastStream *stream, + int32_t *width, + int32_t *height) +{ + *width = stream->width; + *height = stream->height; +} + +void +screen_cast_stream_free (ScreenCastStream *stream) +{ + g_free (stream); +} + +GList * +collect_screen_cast_stream_data (GVariantIter *streams_iter) +{ + GList *streams = NULL; + uint32_t stream_id; + g_autoptr(GVariant) stream_options = NULL; + + while (g_variant_iter_next (streams_iter, "(u@a{sv})", + &stream_id, &stream_options)) + { + ScreenCastStream *stream; + + stream = g_new0 (ScreenCastStream, 1); + stream->id = stream_id; + g_variant_lookup (stream_options, "size", "(ii)", + &stream->width, &stream->height); + + streams = g_list_prepend (streams, stream); + } + + return streams; +} + +static void +replace_restore_screen_cast_data_with_token (ScreenCastSession *screen_cast_session, + GVariant **in_out_results) +{ + xdp_session_persistence_replace_restore_data_with_token ((Session *) screen_cast_session, + SCREEN_CAST_TABLE, + in_out_results, + &screen_cast_session->persist_mode, + &screen_cast_session->restore_token, + &screen_cast_session->restore_data); +} + +static gboolean +process_results (ScreenCastSession *screen_cast_session, + GVariant **in_out_results, + GError **error) +{ + g_autoptr(GVariantIter) streams_iter = NULL; + GVariant *results = *in_out_results; + + if (!g_variant_lookup (results, "streams", "a(ua{sv})", &streams_iter)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "No streams"); + return FALSE; + } + + screen_cast_session->streams = collect_screen_cast_stream_data (streams_iter); + replace_restore_screen_cast_data_with_token (screen_cast_session, + in_out_results); + return TRUE; +} + +static void +start_done (GObject *source_object, + GAsyncResult *res, + gpointer data) +{ + g_autoptr(Request) request = data; + Session *session; + ScreenCastSession *screen_cast_session; + guint response = 2; + gboolean should_close_session; + GVariant *results = NULL; + g_autoptr(GError) error = NULL; + + REQUEST_AUTOLOCK (request); + + session = g_object_get_qdata (G_OBJECT (request), quark_request_session); + SESSION_AUTOLOCK_UNREF (g_object_ref (session)); + g_object_set_qdata (G_OBJECT (request), quark_request_session, NULL); + + if (!xdp_dbus_impl_screen_cast_call_start_finish (impl, + &response, + &results, + res, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("A backend call failed: %s", error->message); + } + + should_close_session = !request->exported || response != 0; + + screen_cast_session = (ScreenCastSession *)session; + + if (request->exported) + { + if (response == 0) + { + if (!process_results (screen_cast_session, &results, &error)) + { + g_warning ("Failed to process results: %s", error->message); + g_clear_error (&error); + g_clear_pointer (&results, g_variant_unref); + response = 2; + should_close_session = TRUE; + } + } + + if (!results) + { + GVariantBuilder results_builder; + + g_variant_builder_init (&results_builder, G_VARIANT_TYPE_VARDICT); + results = g_variant_builder_end (&results_builder); + } + + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), response, results); + request_unexport (request); + } + + if (should_close_session) + { + session_close (session, TRUE); + } + else if (!session->closed) + { + g_assert (screen_cast_session->state == + SCREEN_CAST_SESSION_STATE_STARTING); + g_debug ("screen cast session owned by '%s' started", session->sender); + screen_cast_session->state = SCREEN_CAST_SESSION_STATE_STARTED; + } +} + +static gboolean +handle_start (XdpDbusScreenCast *object, + GDBusMethodInvocation *invocation, + const char *arg_session_handle, + const char *arg_parent_window, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + Session *session; + ScreenCastSession *screen_cast_session; + g_autoptr(GError) error = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + GVariantBuilder options_builder; + GVariant *options; + + REQUEST_AUTOLOCK (request); + + session = acquire_session (arg_session_handle, request); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + screen_cast_session = (ScreenCastSession *)session; + switch (screen_cast_session->state) + { + case SCREEN_CAST_SESSION_STATE_SOURCES_SELECTED: + break; + case SCREEN_CAST_SESSION_STATE_INIT: + case SCREEN_CAST_SESSION_STATE_SELECTING_SOURCES: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Sources not selected"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + case SCREEN_CAST_SESSION_STATE_STARTING: + case SCREEN_CAST_SESSION_STATE_STARTED: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Can only start once"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + case SCREEN_CAST_SESSION_STATE_CLOSED: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_object_set_data_full (G_OBJECT (request), + "window", g_strdup (arg_parent_window), g_free); + + impl_request = + xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, &error); + if (!impl_request) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request (request, impl_request); + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + options = g_variant_builder_end (&options_builder); + + g_object_set_qdata_full (G_OBJECT (request), + quark_request_session, + g_object_ref (session), + g_object_unref); + screen_cast_session->state = SCREEN_CAST_SESSION_STATE_STARTING; + + xdp_dbus_impl_screen_cast_call_start (impl, + request->id, + arg_session_handle, + xdp_app_info_get_id (request->app_info), + arg_parent_window, + options, + NULL, + start_done, + g_object_ref (request)); + + xdp_dbus_screen_cast_complete_start (object, invocation, request->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_open_pipewire_remote (XdpDbusScreenCast *object, + GDBusMethodInvocation *invocation, + GUnixFDList *in_fd_list, + const char *arg_session_handle, + GVariant *arg_options) +{ + Call *call = call_from_invocation (invocation); + Session *session; + GList *streams; + PipeWireRemote *remote; + g_autoptr(GUnixFDList) out_fd_list = NULL; + int fd; + int fd_id; + g_autoptr(GError) error = NULL; + + session = acquire_session_from_call (arg_session_handle, call); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF (session); + + if (is_screen_cast_session (session)) + { + ScreenCastSession *screen_cast_session = (ScreenCastSession *)session; + + streams = screen_cast_session->streams; + } + else if (is_remote_desktop_session (session)) + { + RemoteDesktopSession *remote_desktop_session = + (RemoteDesktopSession *)session; + + streams = remote_desktop_session_get_streams (remote_desktop_session); + } + else + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + if (!streams) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "No streams available"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + remote = open_pipewire_screen_cast_remote (session->app_id, streams, &error); + if (!remote) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "%s", error->message); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + out_fd_list = g_unix_fd_list_new (); + fd = pw_core_steal_fd (remote->core); + fd_id = g_unix_fd_list_append (out_fd_list, fd, &error); + close (fd); + pipewire_remote_destroy (remote); + + if (fd_id == -1) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Failed to append fd: %s", + error->message); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + xdp_dbus_screen_cast_complete_open_pipewire_remote (object, invocation, + out_fd_list, + g_variant_new_handle (fd_id)); + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +screen_cast_iface_init (XdpDbusScreenCastIface *iface) +{ + iface->handle_create_session = handle_create_session; + iface->handle_select_sources = handle_select_sources; + iface->handle_start = handle_start; + iface->handle_open_pipewire_remote = handle_open_pipewire_remote; +} + +static void +sync_supported_source_types (ScreenCast *screen_cast) +{ + unsigned int available_source_types; + + available_source_types = xdp_dbus_impl_screen_cast_get_available_source_types (impl); + xdp_dbus_screen_cast_set_available_source_types (XDP_DBUS_SCREEN_CAST (screen_cast), + available_source_types); +} + +static void +on_supported_source_types_changed (GObject *gobject, + GParamSpec *pspec, + ScreenCast *screen_cast) +{ + sync_supported_source_types (screen_cast); +} + +static void +sync_supported_cursor_modes (ScreenCast *screen_cast) +{ + + available_cursor_modes = xdp_dbus_impl_screen_cast_get_available_cursor_modes (impl); + xdp_dbus_screen_cast_set_available_cursor_modes (XDP_DBUS_SCREEN_CAST (screen_cast), + available_cursor_modes); +} + +static void +on_supported_cursor_modes_changed (GObject *gobject, + GParamSpec *pspec, + ScreenCast *screen_cast) +{ + sync_supported_cursor_modes (screen_cast); +} + +static void +screen_cast_init (ScreenCast *screen_cast) +{ + xdp_dbus_screen_cast_set_version (XDP_DBUS_SCREEN_CAST (screen_cast), 4); + + g_signal_connect (impl, "notify::supported-source-types", + G_CALLBACK (on_supported_source_types_changed), + screen_cast); + if (impl_version >= 2) + { + g_signal_connect (impl, "notify::supported-cursor-modes", + G_CALLBACK (on_supported_cursor_modes_changed), + screen_cast); + } + sync_supported_source_types (screen_cast); + sync_supported_cursor_modes (screen_cast); +} + +static void +screen_cast_class_init (ScreenCastClass *klass) +{ + quark_request_session = + g_quark_from_static_string ("-xdp-request-screen-cast-session"); +} + +GDBusInterfaceSkeleton * +screen_cast_create (GDBusConnection *connection, + const char *dbus_name) +{ + g_autoptr(GError) error = NULL; + + impl = xdp_dbus_impl_screen_cast_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_NONE, + dbus_name, + DESKTOP_PORTAL_OBJECT_PATH, + NULL, + &error); + if (impl == NULL) + { + g_warning ("Failed to create screen cast proxy: %s", error->message); + return NULL; + } + + impl_version = xdp_dbus_impl_screen_cast_get_version (impl); + + g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (impl), G_MAXINT); + + screen_cast = g_object_new (screen_cast_get_type (), NULL); + + return G_DBUS_INTERFACE_SKELETON (screen_cast); +} + +static void +screen_cast_session_close (Session *session) +{ + ScreenCastSession *screen_cast_session = (ScreenCastSession *)session; + + screen_cast_session->state = SCREEN_CAST_SESSION_STATE_CLOSED; + + xdp_session_persistence_generate_and_save_restore_token (session, + SCREEN_CAST_TABLE, + screen_cast_session->persist_mode, + &screen_cast_session->restore_token, + &screen_cast_session->restore_data); + + g_debug ("screen cast session owned by '%s' closed", session->sender); +} + +static void +screen_cast_session_finalize (GObject *object) +{ + ScreenCastSession *screen_cast_session = (ScreenCastSession *)object; + + g_clear_pointer (&screen_cast_session->restore_token, g_free); + g_clear_pointer (&screen_cast_session->restore_data, g_variant_unref); + + g_list_free_full (screen_cast_session->streams, + (GDestroyNotify)screen_cast_stream_free); + + G_OBJECT_CLASS (screen_cast_session_parent_class)->finalize (object); +} + +static void +screen_cast_session_init (ScreenCastSession *screen_cast_session) +{ +} + +static void +screen_cast_session_class_init (ScreenCastSessionClass *klass) +{ + GObjectClass *object_class; + SessionClass *session_class; + + object_class = G_OBJECT_CLASS (klass); + object_class->finalize = screen_cast_session_finalize; + + session_class = (SessionClass *)klass; + session_class->close = screen_cast_session_close; +} diff --git a/src/screen-cast.h b/src/screen-cast.h new file mode 100644 index 0000000..60bc24b --- /dev/null +++ b/src/screen-cast.h @@ -0,0 +1,43 @@ +/* + * Copyright © 2017-2018 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + */ + +#pragma once + +#include +#include + +typedef struct _ScreenCastStream ScreenCastStream; + +uint32_t screen_cast_stream_get_pipewire_node_id (ScreenCastStream *stream); + +void screen_cast_stream_get_size (ScreenCastStream *stream, + int32_t *width, + int32_t *height); + +void screen_cast_stream_free (ScreenCastStream *stream); + +void screen_cast_stream_get_size (ScreenCastStream *stream, + int32_t *width, + int32_t *height); + +void screen_cast_remove_transient_permissions_for_sender (const char *sender); + +GList * collect_screen_cast_stream_data (GVariantIter *streams_iter); + +GDBusInterfaceSkeleton * screen_cast_create (GDBusConnection *connection, + const char *dbus_name); diff --git a/src/screenshot.c b/src/screenshot.c new file mode 100644 index 0000000..91d69e8 --- /dev/null +++ b/src/screenshot.c @@ -0,0 +1,501 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +#include "config.h" + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include "screenshot.h" +#include "permissions.h" +#include "request.h" +#include "documents.h" +#include "xdp-dbus.h" +#include "xdp-impl-dbus.h" +#include "xdp-utils.h" + +#define PERMISSION_TABLE "screenshot" +#define PERMISSION_ID "screenshot" + +typedef struct _Screenshot Screenshot; +typedef struct _ScreenshotClass ScreenshotClass; + +struct _Screenshot +{ + XdpDbusScreenshotSkeleton parent_instance; +}; + +struct _ScreenshotClass +{ + XdpDbusScreenshotSkeletonClass parent_class; +}; + +static XdpDbusImplScreenshot *impl; +static XdpDbusImplAccess *access_impl; +static guint32 impl_version; +static Screenshot *screenshot; + +GType screenshot_get_type (void) G_GNUC_CONST; +static void screenshot_iface_init (XdpDbusScreenshotIface *iface); + +G_DEFINE_TYPE_WITH_CODE (Screenshot, screenshot, XDP_DBUS_TYPE_SCREENSHOT_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_SCREENSHOT, + screenshot_iface_init)); + +static void +send_response (Request *request, + guint response, + GVariant *results) +{ + if (request->exported) + { + g_debug ("sending response: %d", response); + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), response, results); + request_unexport (request); + } +} + +static void +send_response_in_thread_func (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + Request *request = task_data; + GVariantBuilder results; + guint response; + GVariant *options; + g_autoptr(GError) error = NULL; + const char *retval; + + REQUEST_AUTOLOCK (request); + + g_variant_builder_init (&results, G_VARIANT_TYPE_VARDICT); + + response = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (request), "response")); + options = (GVariant *)g_object_get_data (G_OBJECT (request), "options"); + + if (response != 0) + goto out; + + retval = g_object_get_data (G_OBJECT (task), "retval"); + if (g_strcmp0 (retval, "url") == 0) + { + const char *uri; + g_autofree char *ruri = NULL; + + if (!g_variant_lookup (options, "uri", "&s", &uri)) + { + g_warning ("No URI was provided"); + goto out; + } + + if (xdp_app_info_is_host (request->app_info)) + ruri = g_strdup (uri); + else + ruri = register_document (uri, xdp_app_info_get_id (request->app_info), DOCUMENT_FLAG_DELETABLE, &error); + + if (ruri == NULL) + g_warning ("Failed to register %s: %s", uri, error->message); + else + g_variant_builder_add (&results, "{&sv}", "uri", g_variant_new_string (ruri)); + } + else if (g_strcmp0 (retval, "color") == 0) + { + double red, green, blue; + + if (!g_variant_lookup (options, "color", "(ddd)", &red, &green, &blue)) + { + g_warning ("No color was provided"); + goto out; + } + + g_variant_builder_add (&results, "{&sv}", "color", g_variant_new ("(ddd)", red, green, blue)); + } + else + { + g_warning ("Don't know what to return"); + } + +out: + send_response (request, response, g_variant_builder_end (&results)); +} + +static void +screenshot_done (GObject *source, + GAsyncResult *result, + gpointer data) +{ + g_autoptr(Request) request = data; + guint response = 2; + g_autoptr(GVariant) options = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GTask) task = NULL; + + if (!xdp_dbus_impl_screenshot_call_screenshot_finish (XDP_DBUS_IMPL_SCREENSHOT (source), + &response, + &options, + result, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("A backend call failed: %s", error->message); + } + + g_object_set_data (G_OBJECT (request), "response", GINT_TO_POINTER (response)); + if (options) + g_object_set_data_full (G_OBJECT (request), "options", g_variant_ref (options), (GDestroyNotify)g_variant_unref); + + task = g_task_new (NULL, NULL, NULL, NULL); + g_task_set_task_data (task, g_object_ref (request), g_object_unref); + g_object_set_data (G_OBJECT (task), "retval", "url"); + g_task_run_in_thread (task, send_response_in_thread_func); +} + +static XdpOptionKey screenshot_options[] = { + { "modal", G_VARIANT_TYPE_BOOLEAN, NULL }, + { "interactive", G_VARIANT_TYPE_BOOLEAN, NULL } +}; + +static void +handle_screenshot_in_thread_func (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + Request *request = (Request *)task_data; + g_autoptr(GError) error = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + GVariantBuilder opt_builder; + Permission permission; + GVariant *options; + gboolean permission_store_checked = FALSE; + gboolean interactive; + const char *parent_window; + const char *app_id; + + REQUEST_AUTOLOCK (request); + + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + + app_id = xdp_app_info_get_id (request->app_info); + parent_window = ((const char *)g_object_get_data (G_OBJECT (request), "parent-window")); + options = ((GVariant *)g_object_get_data (G_OBJECT (request), "options")); + + if (xdp_dbus_impl_screenshot_get_version (impl) < 2) + goto query_impl; + + permission = get_permission_sync (app_id, PERMISSION_TABLE, PERMISSION_ID); + + if (!g_variant_lookup (options, "interactive", "b", &interactive)) + interactive = FALSE; + + if (!interactive && permission != PERMISSION_YES) + { + g_autoptr(GVariant) access_results = NULL; + GVariantBuilder access_opt_builder; + g_autofree gchar *subtitle = NULL; + g_autofree gchar *title = NULL; + const gchar *body; + guint access_response = 2; + + if (permission == PERMISSION_NO) + { + send_response (request, 2, g_variant_builder_end (&opt_builder)); + return; + } + + g_variant_builder_init (&access_opt_builder, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add (&access_opt_builder, "{sv}", + "deny_label", g_variant_new_string (_("Deny"))); + g_variant_builder_add (&access_opt_builder, "{sv}", + "grant_label", g_variant_new_string (_("Allow"))); + g_variant_builder_add (&access_opt_builder, "{sv}", + "icon", g_variant_new_string ("applets-screenshooter-symbolic")); + + if (g_strcmp0 (app_id, "") != 0) + { + g_autoptr(GDesktopAppInfo) info = NULL; + g_autofree gchar *id = NULL; + const gchar *name; + + id = g_strconcat (app_id, ".desktop", NULL); + info = g_desktop_app_info_new (id); + name = g_app_info_get_display_name (G_APP_INFO (info)); + + title = g_strdup_printf (_("Allow %s to Take Screenshots?"), name); + subtitle = g_strdup_printf (_("%s wants to be able to take screenshots at any time."), name); + } + else + { + /* Note: this will set the wallpaper permission for all unsandboxed + * apps for which an app ID can't be determined. + */ + g_assert (xdp_app_info_is_host (request->app_info)); + title = g_strdup (_("Allow Applications to Take Screenshots?")); + subtitle = g_strdup (_("An application wants to be able to take screenshots at any time.")); + } + + body = _("This permission can be changed at any time from the privacy settings."); + + if (!xdp_dbus_impl_access_call_access_dialog_sync (access_impl, + request->id, + app_id, + parent_window, + title, + subtitle, + body, + g_variant_builder_end (&access_opt_builder), + &access_response, + &access_results, + NULL, + &error)) + { + g_warning ("Failed to show access dialog: %s", error->message); + send_response (request, 2, g_variant_builder_end (&opt_builder)); + return; + } + + if (permission == PERMISSION_UNSET) + set_permission_sync (app_id, PERMISSION_TABLE, PERMISSION_ID, access_response == 0 ? PERMISSION_YES : PERMISSION_NO); + + if (access_response != 0) + { + send_response (request, 2, g_variant_builder_end (&opt_builder)); + return; + } + } + + permission_store_checked = TRUE; + +query_impl: + + impl_request = + xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, &error); + if (!impl_request) + { + g_warning ("Failed to to create screenshot implementation proxy: %s", error->message); + send_response (request, 2, g_variant_builder_end (&opt_builder)); + return; + } + + request_set_impl_request (request, impl_request); + + xdp_filter_options (options, &opt_builder, + screenshot_options, G_N_ELEMENTS (screenshot_options), + NULL); + if (permission_store_checked) + { + g_variant_builder_add (&opt_builder, "{sv}", "permission_store_checked", + g_variant_new_boolean (TRUE)); + } + + g_debug ("Calling Screenshot with interactive=%d", interactive); + xdp_dbus_impl_screenshot_call_screenshot (impl, + request->id, + app_id, + parent_window, + g_variant_builder_end (&opt_builder), + NULL, + screenshot_done, + g_object_ref (request)); + +} + +static gboolean +handle_screenshot (XdpDbusScreenshot *object, + GDBusMethodInvocation *invocation, + const gchar *arg_parent_window, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + g_autoptr(GTask) task = NULL; + + g_debug ("Handle Screenshot"); + + g_object_set_data_full (G_OBJECT (request), "parent-window", g_strdup (arg_parent_window), g_free); + g_object_set_data_full (G_OBJECT (request), + "options", + g_variant_ref (arg_options), + (GDestroyNotify)g_variant_unref); + + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + xdp_dbus_screenshot_complete_screenshot (object, invocation, request->id); + + task = g_task_new (object, NULL, NULL, NULL); + g_task_set_task_data (task, g_object_ref (request), g_object_unref); + g_task_run_in_thread (task, handle_screenshot_in_thread_func); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +pick_color_done (GObject *source, + GAsyncResult *result, + gpointer data) +{ + g_autoptr(Request) request = data; + guint response = 2; + g_autoptr(GVariant) options = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GTask) task = NULL; + + if (!xdp_dbus_impl_screenshot_call_pick_color_finish (XDP_DBUS_IMPL_SCREENSHOT (source), + &response, + &options, + result, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("A backend call failed: %s", error->message); + } + + g_object_set_data (G_OBJECT (request), "response", GINT_TO_POINTER (response)); + if (options) + g_object_set_data_full (G_OBJECT (request), "options", g_variant_ref (options), (GDestroyNotify)g_variant_unref); + + task = g_task_new (NULL, NULL, NULL, NULL); + g_task_set_task_data (task, g_object_ref (request), g_object_unref); + g_object_set_data (G_OBJECT (task), "retval", "color"); + g_task_run_in_thread (task, send_response_in_thread_func); +} + +static XdpOptionKey pick_color_options[] = { +}; + +static gboolean +handle_pick_color (XdpDbusScreenshot *object, + GDBusMethodInvocation *invocation, + const gchar *arg_parent_window, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + g_autoptr(GError) error = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + GVariantBuilder opt_builder; + + REQUEST_AUTOLOCK (request); + + impl_request = + xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, &error); + if (!impl_request) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request (request, impl_request); + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + xdp_filter_options (arg_options, &opt_builder, + pick_color_options, G_N_ELEMENTS (pick_color_options), + NULL); + + xdp_dbus_impl_screenshot_call_pick_color (impl, + request->id, + xdp_app_info_get_id (request->app_info), + arg_parent_window, + g_variant_builder_end (&opt_builder), + NULL, + pick_color_done, + g_object_ref (request)); + + xdp_dbus_screenshot_complete_pick_color (object, invocation, request->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +screenshot_iface_init (XdpDbusScreenshotIface *iface) +{ + iface->handle_screenshot = handle_screenshot; + iface->handle_pick_color = handle_pick_color; +} + +static void +screenshot_init (Screenshot *screenshot) +{ + /* Before there was a version property, the version was hardcoded to 2, so + * make sure we retain that behaviour */ + impl_version = 2; + xdp_dbus_screenshot_set_version (XDP_DBUS_SCREENSHOT (screenshot), + impl_version); +} + +static void +screenshot_class_init (ScreenshotClass *klass) +{ +} + +GDBusInterfaceSkeleton * +screenshot_create (GDBusConnection *connection, + const char *dbus_name_access, + const char *dbus_name_screenshot) +{ + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) version = NULL; + + impl = xdp_dbus_impl_screenshot_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_NONE, + dbus_name_screenshot, + DESKTOP_PORTAL_OBJECT_PATH, + NULL, + &error); + if (impl == NULL) + { + g_warning ("Failed to create screenshot proxy: %s", error->message); + return NULL; + } + + g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (impl), G_MAXINT); + + /* Set the version if supported; otherwise fallback to hardcoded version 2 */ + version = g_dbus_proxy_get_cached_property (G_DBUS_PROXY (impl), "version"); + impl_version = (version != NULL) ? g_variant_get_uint32 (version) : 2; + + screenshot = g_object_new (screenshot_get_type (), NULL); + + access_impl = xdp_dbus_impl_access_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_NONE, + dbus_name_access, + DESKTOP_PORTAL_OBJECT_PATH, + NULL, + &error); + + return G_DBUS_INTERFACE_SKELETON (screenshot); +} diff --git a/src/screenshot.h b/src/screenshot.h new file mode 100644 index 0000000..07577d1 --- /dev/null +++ b/src/screenshot.h @@ -0,0 +1,27 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +#pragma once + +#include + +GDBusInterfaceSkeleton * screenshot_create (GDBusConnection *connection, + const char *dbus_name_access, + const char *dbus_name_screenshot); diff --git a/src/sd-escape.c b/src/sd-escape.c new file mode 100644 index 0000000..25b4492 --- /dev/null +++ b/src/sd-escape.c @@ -0,0 +1,356 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-escape.h" +#include + +/* The following is copied from these files in systemd with minor + * modifications: + * - src/basic/escape.c + * - src/basic/utf8.c + * - src/basic/hexdecoct.c + */ + +static int unhexchar(char c) { + + if (c >= '0' && c <= '9') + return c - '0'; + + if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + + if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + + return -EINVAL; +} + +static gboolean +unichar_is_valid(guint32 ch) { + + if (ch >= 0x110000) /* End of unicode space */ + return FALSE; + if ((ch & 0xFFFFF800) == 0xD800) /* Reserved area for UTF-16 */ + return FALSE; + if ((ch >= 0xFDD0) && (ch <= 0xFDEF)) /* Reserved */ + return FALSE; + if ((ch & 0xFFFE) == 0xFFFE) /* BOM (Byte Order Mark) */ + return FALSE; + + return TRUE; +} + +static int +unoctchar(char c) { + + if (c >= '0' && c <= '7') + return c - '0'; + + return -EINVAL; +} + +int cunescape_one(const char *p, gsize length, guint32 *ret, gboolean *eight_bit, gboolean accept_nul) { + int r = 1; + + g_assert(p); + g_assert(ret); + + /* Unescapes C style. Returns the unescaped character in ret. + * Sets *eight_bit to true if the escaped sequence either fits in + * one byte in UTF-8 or is a non-unicode literal byte and should + * instead be copied directly. + */ + + if (length != G_MAXSIZE && length < 1) + return -EINVAL; + + switch (p[0]) { + + case 'a': + *ret = '\a'; + break; + case 'b': + *ret = '\b'; + break; + case 'f': + *ret = '\f'; + break; + case 'n': + *ret = '\n'; + break; + case 'r': + *ret = '\r'; + break; + case 't': + *ret = '\t'; + break; + case 'v': + *ret = '\v'; + break; + case '\\': + *ret = '\\'; + break; + case '"': + *ret = '"'; + break; + case '\'': + *ret = '\''; + break; + + case 's': + /* This is an extension of the XDG syntax files */ + *ret = ' '; + break; + + case 'x': { + /* hexadecimal encoding */ + int a, b; + + if (length != G_MAXSIZE && length < 3) + return -EINVAL; + + a = unhexchar(p[1]); + if (a < 0) + return -EINVAL; + + b = unhexchar(p[2]); + if (b < 0) + return -EINVAL; + + /* Don't allow NUL bytes */ + if (a == 0 && b == 0 && !accept_nul) + return -EINVAL; + + *ret = (a << 4U) | b; + *eight_bit = TRUE; + r = 3; + break; + } + + case 'u': { + /* C++11 style 16bit unicode */ + + int a[4]; + gsize i; + guint32 c; + + if (length != G_MAXSIZE && length < 5) + return -EINVAL; + + for (i = 0; i < 4; i++) { + a[i] = unhexchar(p[1 + i]); + if (a[i] < 0) + return a[i]; + } + + c = ((guint32) a[0] << 12U) | ((guint32) a[1] << 8U) | ((guint32) a[2] << 4U) | (guint32) a[3]; + + /* Don't allow 0 chars */ + if (c == 0 && !accept_nul) + return -EINVAL; + + *ret = c; + r = 5; + break; + } + + case 'U': { + /* C++11 style 32bit unicode */ + + int a[8]; + gsize i; + guint32 c; + + if (length != G_MAXSIZE && length < 9) + return -EINVAL; + + for (i = 0; i < 8; i++) { + a[i] = unhexchar(p[1 + i]); + if (a[i] < 0) + return a[i]; + } + + c = ((guint32) a[0] << 28U) | ((guint32) a[1] << 24U) | ((guint32) a[2] << 20U) | ((guint32) a[3] << 16U) | + ((guint32) a[4] << 12U) | ((guint32) a[5] << 8U) | ((guint32) a[6] << 4U) | (guint32) a[7]; + + /* Don't allow 0 chars */ + if (c == 0 && !accept_nul) + return -EINVAL; + + /* Don't allow invalid code points */ + if (!unichar_is_valid(c)) + return -EINVAL; + + *ret = c; + r = 9; + break; + } + + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': { + /* octal encoding */ + int a, b, c; + guint32 m; + + if (length != G_MAXSIZE && length < 3) + return -EINVAL; + + a = unoctchar(p[0]); + if (a < 0) + return -EINVAL; + + b = unoctchar(p[1]); + if (b < 0) + return -EINVAL; + + c = unoctchar(p[2]); + if (c < 0) + return -EINVAL; + + /* don't allow NUL bytes */ + if (a == 0 && b == 0 && c == 0 && !accept_nul) + return -EINVAL; + + /* Don't allow bytes above 255 */ + m = ((guint32) a << 6U) | ((guint32) b << 3U) | (guint32) c; + if (m > 255) + return -EINVAL; + + *ret = m; + *eight_bit = TRUE; + r = 3; + break; + } + + default: + return -EINVAL; + } + + return r; +} + +/** + * utf8_encode_unichar() - Encode single UCS-4 character as UTF-8 + * @out_utf8: output buffer of at least 4 bytes or NULL + * @g: UCS-4 character to encode + * + * This encodes a single UCS-4 character as UTF-8 and writes it into @out_utf8. + * The length of the character is returned. It is not zero-terminated! If the + * output buffer is NULL, only the length is returned. + * + * Returns: The length in bytes that the UTF-8 representation does or would + * occupy. + */ +static gsize +utf8_encode_unichar(char *out_utf8, guint32 g) { + + if (g < (1 << 7)) { + if (out_utf8) + out_utf8[0] = g & 0x7f; + return 1; + } else if (g < (1 << 11)) { + if (out_utf8) { + out_utf8[0] = 0xc0 | ((g >> 6) & 0x1f); + out_utf8[1] = 0x80 | (g & 0x3f); + } + return 2; + } else if (g < (1 << 16)) { + if (out_utf8) { + out_utf8[0] = 0xe0 | ((g >> 12) & 0x0f); + out_utf8[1] = 0x80 | ((g >> 6) & 0x3f); + out_utf8[2] = 0x80 | (g & 0x3f); + } + return 3; + } else if (g < (1 << 21)) { + if (out_utf8) { + out_utf8[0] = 0xf0 | ((g >> 18) & 0x07); + out_utf8[1] = 0x80 | ((g >> 12) & 0x3f); + out_utf8[2] = 0x80 | ((g >> 6) & 0x3f); + out_utf8[3] = 0x80 | (g & 0x3f); + } + return 4; + } + + return 0; +} + +gssize cunescape_length_with_prefix(const char *s, gsize length, const char *prefix, UnescapeFlags flags, char **ret) { + g_autofree char *ans = NULL; + char *t; + const char *f; + gsize pl; + int r; + + g_assert(s); + g_assert(ret); + + /* Undoes C style string escaping, and optionally prefixes it. */ + + if (!prefix) + pl = 0; + else + pl = strlen(prefix); + + ans = g_new(char, pl+length+1); + if (!ans) + return -ENOMEM; + + if (prefix) + memcpy(ans, prefix, pl); + + for (f = s, t = ans + pl; f < s + length; f++) { + gsize remaining; + gboolean eight_bit = FALSE; + guint32 u; + + remaining = s + length - f; + g_assert(remaining > 0); + + if (*f != '\\') { + /* A literal, copy verbatim */ + *(t++) = *f; + continue; + } + + if (remaining == 1) { + if (flags & UNESCAPE_RELAX) { + /* A trailing backslash, copy verbatim */ + *(t++) = *f; + continue; + } + + return -EINVAL; + } + + r = cunescape_one(f + 1, remaining - 1, &u, &eight_bit, flags & UNESCAPE_ACCEPT_NUL); + if (r < 0) { + if (flags & UNESCAPE_RELAX) { + /* Invalid escape code, let's take it literal then */ + *(t++) = '\\'; + continue; + } + + return r; + } + + f += r; + if (eight_bit) + /* One byte? Set directly as specified */ + *(t++) = u; + else + /* Otherwise encode as multi-byte UTF-8 */ + t += utf8_encode_unichar(t, u); + } + + *t = 0; + + g_assert(t >= ans); /* Let static analyzers know that the answer is non-negative. */ + *ret = g_steal_pointer (&ans); + return t - *ret; +} + diff --git a/src/sd-escape.h b/src/sd-escape.h new file mode 100644 index 0000000..049f6b1 --- /dev/null +++ b/src/sd-escape.h @@ -0,0 +1,18 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include +#include + +typedef enum UnescapeFlags { + UNESCAPE_RELAX = 1 << 0, + UNESCAPE_ACCEPT_NUL = 1 << 1, +} UnescapeFlags; + +gssize cunescape_length_with_prefix(const char *s, gsize length, const char *prefix, UnescapeFlags flags, char **ret); +static inline gssize cunescape_length(const char *s, gsize length, UnescapeFlags flags, char **ret) { + return cunescape_length_with_prefix(s, length, NULL, flags, ret); +} +static inline gssize cunescape(const char *s, UnescapeFlags flags, char **ret) { + return cunescape_length(s, strlen(s), flags, ret); +} diff --git a/src/secret.c b/src/secret.c new file mode 100644 index 0000000..6f0257b --- /dev/null +++ b/src/secret.c @@ -0,0 +1,217 @@ +/* + * Copyright © 2019 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Daiki Ueno + */ + +#include "config.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include "secret.h" +#include "request.h" +#include "xdp-dbus.h" +#include "xdp-impl-dbus.h" +#include "xdp-utils.h" + +typedef struct _Secret Secret; +typedef struct _SecretClass SecretClass; + +struct _Secret +{ + XdpDbusSecretSkeleton parent_instance; +}; + +struct _SecretClass +{ + XdpDbusSecretSkeletonClass parent_class; +}; + +static XdpDbusImplSecret *impl; +static Secret *secret; + +GType secret_get_type (void) G_GNUC_CONST; +static void secret_iface_init (XdpDbusSecretIface *iface); + +G_DEFINE_TYPE_WITH_CODE (Secret, secret, XDP_DBUS_TYPE_SECRET_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_SECRET, + secret_iface_init)); + +static XdpOptionKey retrieve_secret_options[] = { + { "token", G_VARIANT_TYPE_STRING, NULL }, +}; + +static void +send_response_in_thread_func (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + Request *request = task_data; + guint response; + GVariantBuilder new_results; + + g_variant_builder_init (&new_results, G_VARIANT_TYPE_VARDICT); + + REQUEST_AUTOLOCK (request); + + response = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (request), "response")); + + if (request->exported) + { + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + response, + g_variant_builder_end (&new_results)); + request_unexport (request); + } +} + +static void +retrieve_secret_done (GObject *source, + GAsyncResult *result, + gpointer data) +{ + g_autoptr(Request) request = data; + guint response = 2; + g_autoptr(GVariant) results = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GTask) task = NULL; + + if (!xdp_dbus_impl_secret_call_retrieve_secret_finish (XDP_DBUS_IMPL_SECRET (source), + &response, + &results, + NULL, + result, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("Backend call failed: %s", error->message); + } + + g_object_set_data (G_OBJECT (request), "response", GINT_TO_POINTER (response)); + + task = g_task_new (NULL, NULL, NULL, NULL); + g_task_set_task_data (task, g_object_ref (request), g_object_unref); + g_task_run_in_thread (task, send_response_in_thread_func); +} + +static gboolean +handle_retrieve_secret (XdpDbusSecret *object, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + GVariant *arg_fd, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + const char *app_id = xdp_app_info_get_id (request->app_info); + g_autoptr(GError) error = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + GVariantBuilder options; + + REQUEST_AUTOLOCK (request); + + impl_request = + xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, &error); + if (!impl_request) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT); + + if (!xdp_filter_options (arg_options, &options, + retrieve_secret_options, G_N_ELEMENTS (retrieve_secret_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request (request, impl_request); + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + xdp_dbus_secret_complete_retrieve_secret (object, invocation, NULL, request->id); + + xdp_dbus_impl_secret_call_retrieve_secret (impl, + request->id, + app_id, + arg_fd, + g_variant_builder_end (&options), + fd_list, + NULL, + retrieve_secret_done, + g_object_ref (request)); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +secret_iface_init (XdpDbusSecretIface *iface) +{ + iface->handle_retrieve_secret = handle_retrieve_secret; +} + +static void +secret_init (Secret *secret) +{ + xdp_dbus_secret_set_version (XDP_DBUS_SECRET (secret), 1); +} + +static void +secret_class_init (SecretClass *klass) +{ +} + +GDBusInterfaceSkeleton * +secret_create (GDBusConnection *connection, + const char *dbus_name) +{ + g_autoptr(GError) error = NULL; + + impl = xdp_dbus_impl_secret_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_NONE, + dbus_name, + DESKTOP_PORTAL_OBJECT_PATH, + NULL, + &error); + if (impl == NULL) + { + g_warning ("Failed to create secret proxy: %s", error->message); + return NULL; + } + + g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (impl), G_MAXINT); + + secret = g_object_new (secret_get_type (), NULL); + + return G_DBUS_INTERFACE_SKELETON (secret); +} diff --git a/src/secret.h b/src/secret.h new file mode 100644 index 0000000..9c3e0b1 --- /dev/null +++ b/src/secret.h @@ -0,0 +1,26 @@ +/* + * Copyright © 2019 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Daiki Ueno + */ + +#pragma once + +#include + +GDBusInterfaceSkeleton * secret_create (GDBusConnection *connection, + const char *dbus_name); diff --git a/src/session.c b/src/session.c new file mode 100644 index 0000000..20017de --- /dev/null +++ b/src/session.c @@ -0,0 +1,549 @@ +/* + * Copyright © 2017 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + */ + +#include "session.h" +#include "request.h" +#include "call.h" + +#include + +enum +{ + INTERNAL_CLOSED, + + N_SIGNALS +}; + +static guint signals[N_SIGNALS]; + +enum +{ + PROP_0, + + PROP_SENDER, + PROP_APP_ID, + PROP_TOKEN, + PROP_CONNECTION, + PROP_IMPL_CONNECTION, + PROP_IMPL_DBUS_NAME, + + PROP_LAST +}; + +static GParamSpec *obj_props[PROP_LAST]; + +G_LOCK_DEFINE (sessions); +static GHashTable *sessions; + +static void g_initable_iface_init (GInitableIface *iface); +static void session_skeleton_iface_init (XdpDbusSessionIface *iface); + +G_DEFINE_TYPE_WITH_CODE (Session, session, XDP_DBUS_TYPE_SESSION_SKELETON, + G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, + g_initable_iface_init) + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_SESSION, + session_skeleton_iface_init)) + +#define SESSION_GET_CLASS(o) \ + (G_TYPE_INSTANCE_GET_CLASS ((o), session_get_type (), SessionClass)) + +const char * +lookup_session_token (GVariant *options) +{ + const char *token = NULL; + + g_variant_lookup (options, "session_handle_token", "&s", &token); + + return token; +} + +Session * +acquire_session (const char *session_handle, + Request *request) +{ + g_autoptr(Session) session = NULL; + + G_LOCK (sessions); + session = g_hash_table_lookup (sessions, session_handle); + if (session) + g_object_ref (session); + G_UNLOCK (sessions); + + if (!session) + return NULL; + + if (g_strcmp0 (session->sender, request->sender) != 0) + return NULL; + + if (g_strcmp0 (session->app_id, xdp_app_info_get_id (request->app_info)) != 0) + return NULL; + + return g_steal_pointer (&session); +} + +Session * +acquire_session_from_call (const char *session_handle, + Call *call) +{ + g_autoptr(Session) session = NULL; + + G_LOCK (sessions); + session = g_hash_table_lookup (sessions, session_handle); + if (session) + g_object_ref (session); + G_UNLOCK (sessions); + + if (!session) + return NULL; + + if (g_strcmp0 (session->sender, call->sender) != 0) + return NULL; + + if (g_strcmp0 (session->app_id, xdp_app_info_get_id (call->app_info)) != 0) + return NULL; + + return g_steal_pointer (&session); +} + +Session * +lookup_session (const char *session_handle) +{ + g_autoptr(Session) session = NULL; + + G_LOCK (sessions); + session = g_hash_table_lookup (sessions, session_handle); + if (session) + g_object_ref (session); + G_UNLOCK (sessions); + + return g_steal_pointer (&session); +} + +gboolean +session_export (Session *session, + GError **error) +{ + if (!g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (session), + session->connection, + session->id, + error)) + return FALSE; + + g_object_ref (session); + session->exported = TRUE; + + return TRUE; +} + +static void +session_unexport (Session *session) +{ + session->exported = FALSE; + g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (session)); + g_object_unref (session); +} + +void +session_register (Session *session) +{ + G_LOCK (sessions); + g_hash_table_insert (sessions, session->id, session); + G_UNLOCK (sessions); +} + +static void +session_unregister (Session *session) +{ + G_LOCK (sessions); + g_hash_table_remove (sessions, session->id); + G_UNLOCK (sessions); +} + +void +session_close (Session *session, + gboolean notify_closed) +{ + if (session->closed) + return; + + SESSION_GET_CLASS (session)->close (session); + + g_signal_emit (session, signals[INTERNAL_CLOSED], 0); + + if (notify_closed) + { + GVariantBuilder details_builder; + + g_variant_builder_init (&details_builder, G_VARIANT_TYPE_VARDICT); + g_signal_emit_by_name (session, "closed", + g_variant_builder_end (&details_builder)); + } + + if (session->exported) + session_unexport (session); + + session_unregister (session); + + if (session->impl_session) + { + g_autoptr(GError) error = NULL; + + if (!xdp_dbus_impl_session_call_close_sync (session->impl_session, + NULL, &error)) + g_warning ("Failed to close session implementation: %s", + error->message); + + g_clear_object (&session->impl_session); + } + + session->closed = TRUE; + + + g_object_unref (session); +} + +static gboolean +handle_close (XdpDbusSession *object, + GDBusMethodInvocation *invocation) +{ + Session *session = (Session *)object; + + SESSION_AUTOLOCK_UNREF (g_object_ref (session)); + + session_close (session, FALSE); + + xdp_dbus_session_complete_close (object, invocation); + + return TRUE; +} + +static void +session_skeleton_iface_init (XdpDbusSessionIface *iface) +{ + iface->handle_close = handle_close; +} + +static void +close_sessions_in_thread_func (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + const char *sender = (const char *)task_data; + GSList *list = NULL; + GSList *l; + GHashTableIter iter; + Session *session; + + G_LOCK (sessions); + if (sessions) + { + g_hash_table_iter_init (&iter, sessions); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&session)) + { + if (strcmp (sender, session->sender) == 0) + list = g_slist_prepend (list, g_object_ref (session)); + } + } + G_UNLOCK (sessions); + + for (l = list; l; l = l->next) + { + Session *session = l->data; + + SESSION_AUTOLOCK (session); + session_close (session, FALSE); + } + + g_slist_free_full (list, g_object_unref); +} + +void +close_sessions_for_sender (const char *sender) +{ + g_autoptr(GTask) task = NULL; + + task = g_task_new (NULL, NULL, NULL, NULL); + g_task_set_task_data (task, g_strdup (sender), g_free); + g_task_run_in_thread (task, close_sessions_in_thread_func); +} + +static void +on_closed (XdpDbusImplSession *object, GObject *data) +{ + Session *session = (Session *)data; + + SESSION_AUTOLOCK_UNREF (g_object_ref (session)); + + g_clear_object (&session->impl_session); + session_close (session, TRUE); +} + +static gboolean +session_authorize_callback (GDBusInterfaceSkeleton *interface, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + const gchar *session_owner = user_data; + const gchar *sender = g_dbus_method_invocation_get_sender (invocation); + + if (strcmp (sender, session_owner) != 0) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Portal operation not allowed, Unmatched caller"); + return FALSE; + } + + return TRUE; +} + +static gboolean +session_initable_init (GInitable *initable, + GCancellable *cancellable, + GError **error) +{ + Session *session = (Session *)initable; + g_autofree char *sender_escaped = NULL; + g_autofree char *id = NULL; + g_autoptr(XdpDbusImplSession) impl_session = NULL; + int i; + + sender_escaped = g_strdup (session->sender + 1); + for (i = 0; sender_escaped[i]; i++) + { + if (sender_escaped[i] == '.') + sender_escaped[i] = '_'; + } + + if (!session->token) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Missing token"); + return FALSE; + } + + id = g_strdup_printf ("/org/freedesktop/portal/desktop/session/%s/%s", + sender_escaped, session->token); + + if (session->impl_dbus_name) + { + impl_session = + xdp_dbus_impl_session_proxy_new_sync (session->impl_connection, + G_DBUS_PROXY_FLAGS_NONE, + session->impl_dbus_name, + id, + NULL, error); + if (!impl_session) + return FALSE; + + g_signal_connect (impl_session, "closed", G_CALLBACK (on_closed), session); + + session->impl_session = g_steal_pointer (&impl_session); + } + + session->id = g_steal_pointer (&id); + + g_dbus_interface_skeleton_set_flags (G_DBUS_INTERFACE_SKELETON (session), + G_DBUS_INTERFACE_SKELETON_FLAGS_HANDLE_METHOD_INVOCATIONS_IN_THREAD); + g_signal_connect (session, "g-authorize-method", + G_CALLBACK (session_authorize_callback), + session->sender); + + return TRUE; +} + +static void +g_initable_iface_init (GInitableIface *iface) +{ + iface->init = session_initable_init; +} + +static void +session_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + Session *session = (Session *)object; + + switch (prop_id) + { + case PROP_SENDER: + session->sender = g_strdup (g_value_get_string (value)); + break; + + case PROP_APP_ID: + session->app_id = g_strdup (g_value_get_string (value)); + break; + + case PROP_TOKEN: + session->token = g_strdup (g_value_get_string (value)); + break; + + case PROP_CONNECTION: + g_set_object (&session->connection, g_value_get_object (value)); + break; + + case PROP_IMPL_CONNECTION: + g_set_object (&session->impl_connection, g_value_get_object (value)); + break; + + case PROP_IMPL_DBUS_NAME: + session->impl_dbus_name = g_strdup (g_value_get_string (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +session_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + Session *session = (Session *)object; + + switch (prop_id) + { + case PROP_SENDER: + g_value_set_string (value, session->sender); + break; + + case PROP_APP_ID: + g_value_set_string (value, session->app_id); + break; + + case PROP_TOKEN: + g_value_set_string (value, session->token); + break; + + case PROP_CONNECTION: + g_value_set_object (value, session->connection); + break; + + case PROP_IMPL_CONNECTION: + g_value_set_object (value, session->impl_connection); + break; + + case PROP_IMPL_DBUS_NAME: + g_value_set_string (value, session->impl_dbus_name); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +session_finalize (GObject *object) +{ + Session *session = (Session *)object; + + g_assert (!session->id || !g_hash_table_lookup (sessions, session->id)); + + g_free (session->sender); + g_clear_object (&session->connection); + + g_clear_object (&session->impl_connection); + g_free (session->impl_dbus_name); + g_clear_object (&session->impl_session); + + g_free (session->app_id); + g_free (session->id); + + g_mutex_clear (&session->mutex); + + G_OBJECT_CLASS (session_parent_class)->finalize (object); +} + +static void +session_init (Session *session) +{ + g_mutex_init (&session->mutex); +} + +static void +session_class_init (SessionClass *klass) +{ + GObjectClass *gobject_class; + + sessions = g_hash_table_new_full (g_str_hash, g_str_equal, + NULL, NULL); + + gobject_class = G_OBJECT_CLASS (klass); + gobject_class->finalize = session_finalize; + gobject_class->set_property = session_set_property; + gobject_class->get_property = session_get_property; + + obj_props[PROP_SENDER] = + g_param_spec_string ("sender", "Sender", "Sender", + NULL, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS); + + obj_props[PROP_APP_ID] = + g_param_spec_string ("app-id", "app-id", "App ID", + NULL, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS); + + obj_props[PROP_TOKEN] = + g_param_spec_string ("token", "token", "Token", + NULL, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS); + + obj_props[PROP_CONNECTION] = + g_param_spec_object ("connection", "connection", + "DBus connection", + G_TYPE_DBUS_CONNECTION, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS); + + obj_props[PROP_IMPL_CONNECTION] = + g_param_spec_object ("impl-connection", "impl-connection", + "impl DBus connection", + G_TYPE_DBUS_CONNECTION, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS); + + obj_props[PROP_IMPL_DBUS_NAME] = + g_param_spec_string ("impl-dbus-name", "impl-dbus-name", + "impl DBus name", + NULL, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (gobject_class, PROP_LAST, obj_props); + + signals[INTERNAL_CLOSED] = g_signal_new ("internal-closed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); +} diff --git a/src/session.h b/src/session.h new file mode 100644 index 0000000..d52aa97 --- /dev/null +++ b/src/session.h @@ -0,0 +1,112 @@ +/* + * Copyright © 2017 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + */ + +#pragma once + +#include "request.h" +#include "call.h" +#include "xdp-dbus.h" +#include "xdp-impl-dbus.h" + +typedef struct _Session Session; +typedef struct _SessionClass SessionClass; + +struct _Session +{ + XdpDbusSessionSkeleton parent; + + GMutex mutex; + + gboolean exported; + gboolean closed; + + char *app_id; + char *id; + char *token; + + char *sender; + GDBusConnection *connection; + + char *impl_dbus_name; + GDBusConnection *impl_connection; + XdpDbusImplSession *impl_session; + + struct { + gboolean has_transient_permissions; + } persistence; +}; + +struct _SessionClass +{ + XdpDbusSessionSkeletonClass parent_class; + + void (*close) (Session *session); +}; + +GType session_get_type (void); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (Session, g_object_unref) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (XdpDbusImplSession, g_object_unref) + +const char * lookup_session_token (GVariant *options); + +Session * acquire_session (const char *session_handle, + Request *request); + +Session * acquire_session_from_call (const char *session_handle, + Call *call); + +Session * lookup_session (const char *session_handle); + +void session_register (Session *session); + +gboolean session_export (Session *session, + GError **error); + +void close_sessions_for_sender (const char *sender); + +void session_close (Session *session, + gboolean notify_close); + +static inline void +auto_session_unlock_unref_helper (Session **session) +{ + if (!*session) + return; + + g_mutex_unlock (&(*session)->mutex); + g_object_unref (*session); +} + +static inline Session * +auto_session_lock_helper (Session *session) +{ + if (session) + g_mutex_lock (&session->mutex); + return session; +} + +#define SESSION_AUTOLOCK(session) \ + G_GNUC_UNUSED __attribute__((cleanup (auto_unlock_helper))) \ + GMutex * G_PASTE (session_auto_unlock, __LINE__) = \ + auto_lock_helper (&session->mutex); + +#define SESSION_AUTOLOCK_UNREF(session) \ + G_GNUC_UNUSED __attribute__((cleanup (auto_session_unlock_unref_helper))) \ + Session * G_PASTE (session_auto_unlock_unref, __LINE__) = \ + auto_session_lock_helper (session); diff --git a/src/settings.c b/src/settings.c new file mode 100644 index 0000000..4a60cdd --- /dev/null +++ b/src/settings.c @@ -0,0 +1,220 @@ +/* + * Copyright © 2018 Igalia S.L. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Patrick Griffis + */ + +#include "config.h" + +#include +#include +#include +#include + +#include "settings.h" +#include "xdp-dbus.h" +#include "xdp-impl-dbus.h" +#include "xdp-utils.h" +#include "portal-impl.h" + +typedef struct _Settings Settings; +typedef struct _SettingsClass SettingsClass; + +struct _Settings +{ + XdpDbusSettingsSkeleton parent_instance; +}; + +struct _SettingsClass +{ + XdpDbusSettingsSkeletonClass parent_class; +}; + +static XdpDbusImplSettings **impls; +static int n_impls = 0; + +GType settings_get_type (void) G_GNUC_CONST; +static void settings_iface_init (XdpDbusSettingsIface *iface); + +G_DEFINE_TYPE_WITH_CODE (Settings, settings, XDP_DBUS_TYPE_SETTINGS_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_SETTINGS, + settings_iface_init)); + +static gboolean +settings_handle_read_all (XdpDbusSettings *object, + GDBusMethodInvocation *invocation, + const char * const *arg_namespaces) +{ + g_autoptr(GVariantBuilder) builder = g_variant_builder_new (G_VARIANT_TYPE ("(a{sa{sv}})")); + int j; + + g_variant_builder_open (builder, G_VARIANT_TYPE ("a{sa{sv}}")); + + for (j = 0; j < n_impls; j++) + { + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) impl_value = NULL; + + if (!xdp_dbus_impl_settings_call_read_all_sync (impls[j], arg_namespaces, + &impl_value, NULL, &error)) + { + g_warning ("Failed to ReadAll() from Settings implementation: %s", error->message); + } + else + { + size_t i; + + for (i = 0; i < g_variant_n_children (impl_value); ++i) + { + g_autoptr(GVariant) child = g_variant_get_child_value (impl_value, i); + g_variant_builder_add_value (builder, child); + } + } + } + + g_variant_builder_close (builder); + + g_dbus_method_invocation_return_value (invocation, g_variant_builder_end (builder)); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +settings_handle_read (XdpDbusSettings *object, + GDBusMethodInvocation *invocation, + const char *arg_namespace, + const char *arg_key) +{ + int i; + + g_debug ("Read %s %s", arg_namespace, arg_key); + + for (i = 0; i < n_impls; i++) + { + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) impl_value = NULL; + + if (!xdp_dbus_impl_settings_call_read_sync (impls[i], arg_namespace, + arg_key, &impl_value, NULL, &error)) + { + /* A key not being found is expected, continue to our implementation */ + g_debug ("Failed to Read() from Settings implementation: %s", error->message); + } + else + { + g_dbus_method_invocation_return_value (invocation, g_variant_new ("(v)", impl_value)); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + } + + g_debug ("Attempted to read unknown namespace/key pair: %s %s", arg_namespace, arg_key); + g_dbus_method_invocation_return_error_literal (invocation, XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_FOUND, + _("Requested setting not found")); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +on_impl_settings_changed (XdpDbusImplSettings *impl, + const char *arg_namespace, + const char *arg_key, + GVariant *arg_value, + XdpDbusSettings *settings) +{ + g_debug ("Emitting changed for %s %s", arg_namespace, arg_key); + xdp_dbus_settings_emit_setting_changed (settings, arg_namespace, + arg_key, arg_value); +} + +static void +settings_iface_init (XdpDbusSettingsIface *iface) +{ + iface->handle_read = settings_handle_read; + iface->handle_read_all = settings_handle_read_all; +} + +static void +settings_init (Settings *settings) +{ + xdp_dbus_settings_set_version (XDP_DBUS_SETTINGS (settings), 1); +} + +static void +settings_finalize (GObject *object) +{ + Settings *self = (Settings*)object; + int i; + + for (i = 0; i < n_impls; i++) + g_signal_handlers_disconnect_by_data (impls[i], self); + + G_OBJECT_CLASS (settings_parent_class)->finalize (object); +} + +static void +settings_class_init (SettingsClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = settings_finalize; +} + +GDBusInterfaceSkeleton * +settings_create (GDBusConnection *connection, + GPtrArray *implementations) +{ + Settings *settings; + g_autoptr(GError) error = NULL; + int i; + int n_impls_tmp; + + n_impls_tmp = implementations->len; + impls = g_new (XdpDbusImplSettings *, n_impls_tmp); + + settings = g_object_new (settings_get_type (), NULL); + + for (i = 0; i < n_impls_tmp; i++) + { + PortalImplementation *impl = g_ptr_array_index (implementations, i); + const char *dbus_name = impl->dbus_name; + + XdpDbusImplSettings *impl_proxy = + xdp_dbus_impl_settings_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_NONE, + dbus_name, + DESKTOP_PORTAL_OBJECT_PATH, + NULL, + &error); + if (impl_proxy == NULL) + { + g_warning ("Failed to create settings proxy: %s", error->message); + } + else + { + impls[n_impls++] = impl_proxy; + g_signal_connect (impl_proxy, "setting-changed", G_CALLBACK (on_impl_settings_changed), settings); + } + } + + if (!n_impls) + { + return NULL; + } + + return G_DBUS_INTERFACE_SKELETON (settings); +} diff --git a/src/settings.h b/src/settings.h new file mode 100644 index 0000000..a6419e0 --- /dev/null +++ b/src/settings.h @@ -0,0 +1,27 @@ +/* + * Copyright © 2018 Igalia S.L. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Patrick Griffis + */ + + +#pragma once + +#include + +GDBusInterfaceSkeleton * settings_create (GDBusConnection *connection, + GPtrArray *impls); diff --git a/src/trash.c b/src/trash.c new file mode 100644 index 0000000..71920d0 --- /dev/null +++ b/src/trash.c @@ -0,0 +1,145 @@ +/* + * Copyright © 2018 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +#include "config.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include "trash.h" +#include "request.h" +#include "documents.h" +#include "xdp-dbus.h" +#include "xdp-impl-dbus.h" +#include "xdp-utils.h" + +typedef struct _Trash Trash; +typedef struct _TrashClass TrashClass; + +struct _Trash +{ + XdpDbusTrashSkeleton parent_instance; +}; + +struct _TrashClass +{ + XdpDbusTrashSkeletonClass parent_class; +}; + +static Trash *trash; + +GType trash_get_type (void) G_GNUC_CONST; +static void trash_iface_init (XdpDbusTrashIface *iface); + +G_DEFINE_TYPE_WITH_CODE (Trash, trash, XDP_DBUS_TYPE_TRASH_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_TRASH, + trash_iface_init)); + +static guint +trash_file (XdpAppInfo *app_info, + const char *sender, + int fd) +{ + g_autofree char *path = NULL; + gboolean writable; + g_autoptr(GFile) file = NULL; + g_autoptr(GError) local_error = NULL; + + path = xdp_app_info_get_path_for_fd (app_info, fd, 0, NULL, &writable, &local_error); + + if (path == NULL) + { + g_debug ("Cannot trash file with invalid fd: %s", local_error->message); + return 0; + } + + if (!writable) + { + g_debug ("Cannot trash file \"%s\": not opened for writing", path); + return 0; + } + + file = g_file_new_for_path (path); + if (!g_file_trash (file, NULL, &local_error)) + { + g_debug ("Cannot trash file \"%s\": %s", path, local_error->message); + return 0; + } + + return 1; +} + +static gboolean +handle_trash_file (XdpDbusTrash *object, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + GVariant *arg_fd) +{ + Request *request = request_from_invocation (invocation); + int idx, fd; + guint result; + + g_debug ("Handling TrashFile"); + + REQUEST_AUTOLOCK (request); + + g_variant_get (arg_fd, "h", &idx); + fd = g_unix_fd_list_get (fd_list, idx, NULL); + + result = trash_file (request->app_info, request->sender, fd); + + xdp_dbus_trash_complete_trash_file (object, invocation, NULL, result); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +trash_iface_init (XdpDbusTrashIface *iface) +{ + iface->handle_trash_file = handle_trash_file; +} + +static void +trash_init (Trash *trash) +{ + xdp_dbus_trash_set_version (XDP_DBUS_TRASH (trash), 1); +} + +static void +trash_class_init (TrashClass *klass) +{ +} + +GDBusInterfaceSkeleton * +trash_create (GDBusConnection *connection) +{ + trash = g_object_new (trash_get_type (), NULL); + + return G_DBUS_INTERFACE_SKELETON (trash); +} diff --git a/src/trash.h b/src/trash.h new file mode 100644 index 0000000..484b2e2 --- /dev/null +++ b/src/trash.h @@ -0,0 +1,25 @@ +/* + * Copyright © 2018 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +#pragma once + +#include + +GDBusInterfaceSkeleton * trash_create (GDBusConnection *connection); diff --git a/src/validate-icon.c b/src/validate-icon.c new file mode 100644 index 0000000..8aee542 --- /dev/null +++ b/src/validate-icon.c @@ -0,0 +1,286 @@ +/* + * Copyright © 2018 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +/* The canonical copy of this file is in: + * - https://github.com/flatpak/flatpak at icon-validator/validate-icon.c + * Known copies of this file are in: + * - https://github.com/flatpak/xdg-desktop-portal at src/validate-icon.c + */ + +#include +#include +#include +#include + +#ifdef __FreeBSD__ +#define execvpe exect +#endif + +#define ICON_VALIDATOR_GROUP "Icon Validator" + +static int +validate_icon (const char *arg_width, + const char *arg_height, + const char *filename) +{ + GdkPixbufFormat *format; + int max_width, max_height; + int width, height; + const char *name; + const char *allowed_formats[] = { "png", "jpeg", "svg", NULL }; + g_autoptr(GdkPixbuf) pixbuf = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GKeyFile) key_file = NULL; + g_autofree char *key_file_data = NULL; + + format = gdk_pixbuf_get_file_info (filename, &width, &height); + if (format == NULL) + { + g_printerr ("Format not recognized\n"); + return 1; + } + + name = gdk_pixbuf_format_get_name (format); + if (!g_strv_contains (allowed_formats, name)) + { + g_printerr ("Format %s not accepted\n", name); + return 1; + } + + if (!g_str_equal (name, "svg")) + { + max_width = g_ascii_strtoll (arg_width, NULL, 10); + if (max_width < 16 || max_width > 4096) + { + g_printerr ("Bad width limit: %s\n", arg_width); + return 1; + } + + max_height = g_ascii_strtoll (arg_height, NULL, 10); + if (max_height < 16 || max_height > 4096) + { + g_printerr ("Bad height limit: %s\n", arg_height); + return 1; + } + } + else + { + /* Sanity check for vector files */ + max_height = max_width = 4096; + } + + if (width > max_width || height > max_height) + { + g_printerr ("Image too large (%dx%d). Max. size %dx%d\n", width, height, max_width, max_height); + return 1; + } + + pixbuf = gdk_pixbuf_new_from_file (filename, &error); + if (pixbuf == NULL) + { + g_printerr ("Failed to load image: %s\n", error->message); + return 1; + } + + if (width != height) + { + g_printerr ("Expected a square icon but got: %dx%d\n", width, height); + return 1; + } + + /* Print the format and size for consumption by (at least) the dynamic + * launcher portal. xdg-desktop-portal has a copy of this file. Use a + * GKeyFile so the output can be easily extended in the future in a backwards + * compatible way. + */ + key_file = g_key_file_new (); + g_key_file_set_string (key_file, ICON_VALIDATOR_GROUP, "format", name); + g_key_file_set_integer (key_file, ICON_VALIDATOR_GROUP, "width", width); + key_file_data = g_key_file_to_data (key_file, NULL, NULL); + g_print ("%s", key_file_data); + + return 0; +} + +G_GNUC_NULL_TERMINATED +static void +add_args (GPtrArray *argv_array, ...) +{ + va_list args; + const char *arg; + + va_start (args, argv_array); + while ((arg = va_arg (args, const gchar *))) + g_ptr_array_add (argv_array, g_strdup (arg)); + va_end (args); +} + +const char * +flatpak_get_bwrap (void) +{ + const char *e = g_getenv ("FLATPAK_BWRAP"); + + if (e != NULL) + return e; +#ifdef HELPER + return HELPER; +#else + return NULL; +#endif +} + + +static gboolean +path_is_usrmerged (const char *dir) +{ + /* does /dir point to /usr/dir? */ + g_autofree char *target = NULL; + GStatBuf stat_buf_src, stat_buf_target; + + if (g_stat (dir, &stat_buf_src) < 0) + return FALSE; + + target = g_strdup_printf ("/usr/%s", dir); + + if (g_stat (target, &stat_buf_target) < 0) + return FALSE; + + return (stat_buf_src.st_dev == stat_buf_target.st_dev) && + (stat_buf_src.st_ino == stat_buf_target.st_ino); +} + +static int +rerun_in_sandbox (const char *arg_width, + const char *arg_height, + const char *filename) +{ + const char * const usrmerged_dirs[] = { "bin", "lib32", "lib64", "lib", "sbin" }; + int i; + g_autoptr(GPtrArray) args = g_ptr_array_new_with_free_func (g_free); + char validate_icon[PATH_MAX + 1]; + ssize_t symlink_size; + + symlink_size = readlink ("/proc/self/exe", validate_icon, sizeof (validate_icon) - 1); + if (symlink_size < 0 || (size_t) symlink_size >= sizeof (validate_icon)) + { + g_printerr ("Error: failed to read /proc/self/exe\n"); + return 1; + } + + validate_icon[symlink_size] = 0; + + add_args (args, + flatpak_get_bwrap (), + "--unshare-ipc", + "--unshare-net", + "--unshare-pid", + "--ro-bind", "/usr", "/usr", + "--ro-bind-try", "/etc/ld.so.cache", "/etc/ld.so.cache", + "--ro-bind", validate_icon, validate_icon, + NULL); + + /* These directories might be symlinks into /usr/... */ + for (i = 0; i < G_N_ELEMENTS (usrmerged_dirs); i++) + { + g_autofree char *absolute_dir = g_strdup_printf ("/%s", usrmerged_dirs[i]); + + if (!g_file_test (absolute_dir, G_FILE_TEST_EXISTS)) + continue; + + if (path_is_usrmerged (absolute_dir)) + { + g_autofree char *symlink_target = g_strdup_printf ("/usr/%s", absolute_dir); + + add_args (args, + "--symlink", symlink_target, absolute_dir, + NULL); + } + else + { + add_args (args, + "--ro-bind", absolute_dir, absolute_dir, + NULL); + } + } + + add_args (args, + "--tmpfs", "/tmp", + "--proc", "/proc", + "--dev", "/dev", + "--chdir", "/", + "--setenv", "GIO_USE_VFS", "local", + "--unsetenv", "TMPDIR", + "--die-with-parent", + "--ro-bind", filename, filename, + NULL); + + if (g_getenv ("G_MESSAGES_DEBUG")) + add_args (args, "--setenv", "G_MESSAGES_DEBUG", g_getenv ("G_MESSAGES_DEBUG"), NULL); + if (g_getenv ("G_MESSAGES_PREFIXED")) + add_args (args, "--setenv", "G_MESSAGES_PREFIXED", g_getenv ("G_MESSAGES_PREFIXED"), NULL); + + add_args (args, validate_icon, arg_width, arg_height, filename, NULL); + g_ptr_array_add (args, NULL); + + { + g_autofree char *cmdline = g_strjoinv (" ", (char **) args->pdata); + g_debug ("Icon validation: Spawning %s", cmdline); + } + + execvpe (flatpak_get_bwrap (), (char **) args->pdata, NULL); + /* If we get here, then execvpe() failed. */ + g_printerr ("Icon validation: execvpe %s: %s\n", flatpak_get_bwrap (), g_strerror (errno)); + return 1; +} + +static gboolean opt_sandbox; + +static GOptionEntry entries[] = { + { "sandbox", 0, 0, G_OPTION_ARG_NONE, &opt_sandbox, "Run in a sandbox", NULL }, + { NULL } +}; + +int +main (int argc, char *argv[]) +{ + GOptionContext *context; + GError *error = NULL; + + context = g_option_context_new ("WIDTH HEIGHT PATH"); + g_option_context_add_main_entries (context, entries, NULL); + if (!g_option_context_parse (context, &argc, &argv, &error)) + { + g_printerr ("Error: %s\n", error->message); + return 1; + } + + if (argc != 4) + { + g_printerr ("Usage: %s [OPTION…] WIDTH HEIGHT PATH\n", argv[0]); + return 1; + } + +#ifdef HELPER + if (opt_sandbox) + return rerun_in_sandbox (argv[1], argv[2], argv[3]); + else +#endif + return validate_icon (argv[1], argv[2], argv[3]); +} diff --git a/src/wallpaper.c b/src/wallpaper.c new file mode 100644 index 0000000..4f1b501 --- /dev/null +++ b/src/wallpaper.c @@ -0,0 +1,414 @@ +/* + * Copyright © 2019 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Felipe Borges + */ + +#include "config.h" + +#include +#include +#include +#include +#include + +#include "wallpaper.h" +#include "permissions.h" +#include "request.h" +#include "xdp-dbus.h" +#include "xdp-impl-dbus.h" +#include "xdp-utils.h" + +#define PERMISSION_TABLE "wallpaper" +#define PERMISSION_ID "wallpaper" + +typedef struct _Wallpaper Wallpaper; +typedef struct _WallpaperClass WallpaperClass; + +struct _Wallpaper +{ + XdpDbusWallpaperSkeleton parent_instance; +}; + +struct _WallpaperClass +{ + XdpDbusWallpaperSkeletonClass parent_class; +}; + +static XdpDbusImplWallpaper *impl; +static XdpDbusImplAccess *access_impl; +static Wallpaper *wallpaper; + +GType wallpaper_get_type (void) G_GNUC_CONST; +static void wallpaper_iface_init (XdpDbusWallpaperIface *iface); + +G_DEFINE_TYPE_WITH_CODE (Wallpaper, wallpaper, XDP_DBUS_TYPE_WALLPAPER_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_WALLPAPER, + wallpaper_iface_init)); + +static void +send_response (Request *request, + guint response) +{ + if (request->exported) + { + GVariantBuilder opt_builder; + + g_debug ("sending response: %d", response); + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + response, + g_variant_builder_end (&opt_builder)); + request_unexport (request); + } +} + +static void +handle_set_wallpaper_uri_done (GObject *source, + GAsyncResult *result, + gpointer data) +{ + guint response = 2; + g_autoptr(GError) error = NULL; + Request *request = data; + + if (!xdp_dbus_impl_wallpaper_call_set_wallpaper_uri_finish (XDP_DBUS_IMPL_WALLPAPER (source), + &response, + result, + &error)) + { + g_dbus_error_strip_remote_error (error); + g_warning ("A backend call failed: %s", error->message); + } + + send_response (request, response); + g_object_unref (request); +} + +static gboolean +validate_set_on (const char *key, + GVariant *value, + GVariant *options, + GError **error) +{ + const char *string = g_variant_get_string (value, NULL); + + return ((g_strcmp0 (string, "both") == 0) || + (g_strcmp0 (string, "background") == 0) || + (g_strcmp0 (string, "lockscreen") == 0)); +} + +static XdpOptionKey wallpaper_options[] = { + { "show-preview", G_VARIANT_TYPE_BOOLEAN, NULL }, + { "set-on", G_VARIANT_TYPE_STRING, validate_set_on } +}; + +static void +handle_set_wallpaper_in_thread_func (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + Request *request = (Request *)task_data; + const char *parent_window; + const char *app_id = xdp_app_info_get_id (request->app_info); + g_autoptr(GError) error = NULL; + g_autofree char *uri = NULL; + GVariantBuilder opt_builder; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + GVariant *options; + gboolean show_preview = FALSE; + int fd; + Permission permission; + + REQUEST_AUTOLOCK (request); + + parent_window = ((const char *)g_object_get_data (G_OBJECT (request), "parent-window")); + uri = g_strdup ((const char *)g_object_get_data (G_OBJECT (request), "uri")); + fd = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (request), "fd")); + options = ((GVariant *)g_object_get_data (G_OBJECT (request), "options")); + + if (uri != NULL && fd != -1) + { + g_warning ("Rejecting invalid set-wallpaper request (both URI and fd are set)"); + if (request->exported) + { + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + XDG_DESKTOP_PORTAL_RESPONSE_OTHER, + g_variant_builder_end (&opt_builder)); + request_unexport (request); + } + return; + } + + + permission = get_permission_sync (app_id, PERMISSION_TABLE, PERMISSION_ID); + + if (permission == PERMISSION_NO) + { + send_response (request, 2); + return; + } + + g_variant_lookup (options, "show-preview", "b", &show_preview); + if (!show_preview && permission != PERMISSION_YES) + { + guint access_response = 2; + g_autoptr(GVariant) access_results = NULL; + GVariantBuilder access_opt_builder; + g_autofree gchar *title = NULL; + g_autofree gchar *subtitle = NULL; + const gchar *body; + + g_variant_builder_init (&access_opt_builder, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add (&access_opt_builder, "{sv}", + "deny_label", g_variant_new_string (_("Deny"))); + g_variant_builder_add (&access_opt_builder, "{sv}", + "grant_label", g_variant_new_string (_("Allow"))); + g_variant_builder_add (&access_opt_builder, "{sv}", + "icon", g_variant_new_string ("preferences-desktop-wallpaper-symbolic")); + + if (g_str_equal (app_id, "")) + { + /* Note: this will set the wallpaper permission for all unsandboxed + * apps for which an app ID can't be determined. + */ + g_assert (xdp_app_info_is_host (request->app_info)); + title = g_strdup (_("Allow Applications to Set Backgrounds?")); + subtitle = g_strdup (_("An application is requesting to be able to change the background image.")); + } + else + { + g_autoptr(GDesktopAppInfo) info = NULL; + g_autofree gchar *id = NULL; + const gchar *name; + + id = g_strconcat (app_id, ".desktop", NULL); + info = g_desktop_app_info_new (id); + name = g_app_info_get_display_name (G_APP_INFO (info)); + + title = g_strdup_printf (_("Allow %s to Set Backgrounds?"), name); + subtitle = g_strdup_printf (_("%s is requesting to be able to change the background image."), name); + } + + body = _("This permission can be changed at any time from the privacy settings."); + + if (!xdp_dbus_impl_access_call_access_dialog_sync (access_impl, + request->id, + app_id, + parent_window, + title, + subtitle, + body, + g_variant_builder_end (&access_opt_builder), + &access_response, + &access_results, + NULL, + &error)) + { + g_warning ("Failed to show access dialog: %s", error->message); + send_response (request, 2); + return; + } + + if (permission == PERMISSION_UNSET) + set_permission_sync (app_id, PERMISSION_TABLE, PERMISSION_ID, access_response == 0 ? PERMISSION_YES : PERMISSION_NO); + + if (access_response != 0) + { + send_response (request, 2); + return; + } + } + + if (!uri) + { + g_autofree char *path = NULL; + + path = xdp_app_info_get_path_for_fd (request->app_info, fd, 0, NULL, NULL, &error); + if (path == NULL) + { + g_debug ("Cannot get path for fd: %s", error->message); + + /* Reject the request */ + if (request->exported) + { + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + XDG_DESKTOP_PORTAL_RESPONSE_OTHER, + g_variant_builder_end (&opt_builder)); + request_unexport (request); + } + return; + } + + uri = g_filename_to_uri (path, NULL, NULL); + g_object_set_data_full (G_OBJECT (request), "uri", g_strdup (uri), g_free); + close (fd); + g_object_set_data (G_OBJECT (request), "fd", GINT_TO_POINTER (-1)); + } + + impl_request = xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (impl)), + request->id, + NULL, &error); + + if (!impl_request) + { + g_warning ("Failed to to create wallpaper implementation proxy: %s", error->message); + send_response (request, 2); + return; + } + + request_set_impl_request (request, impl_request); + + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + xdp_filter_options (options, &opt_builder, + wallpaper_options, G_N_ELEMENTS (wallpaper_options), + NULL); + + g_debug ("Calling SetWallpaperURI with %s", uri); + xdp_dbus_impl_wallpaper_call_set_wallpaper_uri (impl, + request->id, + app_id, + parent_window, + uri, + g_variant_builder_end (&opt_builder), + NULL, + handle_set_wallpaper_uri_done, + g_object_ref (request)); +} + +static gboolean +handle_set_wallpaper_uri (XdpDbusWallpaper *object, + GDBusMethodInvocation *invocation, + const char *arg_parent_window, + const char *arg_uri, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + g_autoptr(GTask) task = NULL; + + g_debug ("Handle SetWallpaperURI"); + + g_object_set_data_full (G_OBJECT (request), "uri", g_strdup (arg_uri), g_free); + g_object_set_data_full (G_OBJECT (request), "parent-window", g_strdup (arg_parent_window), g_free); + g_object_set_data_full (G_OBJECT (request), + "options", + g_variant_ref (arg_options), + (GDestroyNotify)g_variant_unref); + + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + xdp_dbus_wallpaper_complete_set_wallpaper_uri (object, invocation, request->id); + + task = g_task_new (object, NULL, NULL, NULL); + g_task_set_task_data (task, g_object_ref (request), g_object_unref); + g_task_run_in_thread (task, handle_set_wallpaper_in_thread_func); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_set_wallpaper_file (XdpDbusWallpaper *object, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + const char *arg_parent_window, + GVariant *arg_fd, + GVariant *arg_options) +{ + Request *request = request_from_invocation (invocation); + g_autoptr(GTask) task = NULL; + int fd_id, fd; + g_autoptr(GError) error = NULL; + + g_debug ("Handle SetWallpaperFile"); + + g_variant_get (arg_fd, "h", &fd_id); + fd = g_unix_fd_list_get (fd_list, fd_id, &error); + if (fd == -1) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_object_set_data (G_OBJECT (request), "fd", GINT_TO_POINTER (fd)); + g_object_set_data_full (G_OBJECT (request), "parent-window", g_strdup (arg_parent_window), g_free); + g_object_set_data_full (G_OBJECT (request), + "options", + g_variant_ref (arg_options), + (GDestroyNotify)g_variant_unref); + + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + xdp_dbus_wallpaper_complete_set_wallpaper_file (object, invocation, NULL, request->id); + + task = g_task_new (object, NULL, NULL, NULL); + g_task_set_task_data (task, g_object_ref (request), g_object_unref); + g_task_run_in_thread (task, handle_set_wallpaper_in_thread_func); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} +static void +wallpaper_iface_init (XdpDbusWallpaperIface *iface) +{ + iface->handle_set_wallpaper_uri = handle_set_wallpaper_uri; + iface->handle_set_wallpaper_file = handle_set_wallpaper_file; +} + +static void +wallpaper_init (Wallpaper *wallpaper) +{ + xdp_dbus_wallpaper_set_version (XDP_DBUS_WALLPAPER (wallpaper), 1); +} + +static void +wallpaper_class_init (WallpaperClass *klass) +{ +} + +GDBusInterfaceSkeleton * +wallpaper_create (GDBusConnection *connection, + const char *dbus_name_access, + const char *dbus_name_wallpaper) +{ + g_autoptr(GError) error = NULL; + + impl = xdp_dbus_impl_wallpaper_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_NONE, + dbus_name_wallpaper, + DESKTOP_PORTAL_OBJECT_PATH, + NULL, + &error); + if (impl == NULL) + { + g_warning ("Failed to create wallpaper proxy: %s", error->message); + return NULL; + } + + g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (impl), G_MAXINT); + wallpaper = g_object_new (wallpaper_get_type (), NULL); + + access_impl = xdp_dbus_impl_access_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_NONE, + dbus_name_access, + DESKTOP_PORTAL_OBJECT_PATH, + NULL, + &error); + + return G_DBUS_INTERFACE_SKELETON (wallpaper); +} diff --git a/src/wallpaper.h b/src/wallpaper.h new file mode 100644 index 0000000..19ec2ad --- /dev/null +++ b/src/wallpaper.h @@ -0,0 +1,27 @@ +/* + * Copyright © 2019 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Felipe Borges + */ + +#pragma once + +#include + +GDBusInterfaceSkeleton * wallpaper_create (GDBusConnection *connection, + const char *dbus_name_access, + const char *dbus_name_wallpaper); diff --git a/src/xdg-desktop-portal-rewrite-launchers.service.in b/src/xdg-desktop-portal-rewrite-launchers.service.in new file mode 100644 index 0000000..c907df8 --- /dev/null +++ b/src/xdg-desktop-portal-rewrite-launchers.service.in @@ -0,0 +1,13 @@ +[Unit] +Description=Rewrite dynamic launcher portal entries +# Do it before the graphical session in case the launchers we need to rewrite +# are configured to launch automatically +PartOf=graphical-session-pre.target + +[Service] +Type=oneshot +ExecStart=@libexecdir@/xdg-desktop-portal-rewrite-launchers +Slice=session.slice + +[Install] +WantedBy=graphical-session-pre.target diff --git a/src/xdg-desktop-portal.c b/src/xdg-desktop-portal.c new file mode 100644 index 0000000..1577525 --- /dev/null +++ b/src/xdg-desktop-portal.c @@ -0,0 +1,489 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Alexander Larsson + * Matthias Clasen + */ + +#include "config.h" + +#include +#include +#include + +#include + +#include "xdp-utils.h" +#include "xdp-dbus.h" +#include "xdp-impl-dbus.h" +#include "account.h" +#include "background.h" +#include "call.h" +#include "camera.h" +#include "clipboard.h" +#include "device.h" +#include "documents.h" +#include "dynamic-launcher.h" +#include "email.h" +#include "file-chooser.h" +#include "gamemode.h" +#include "global-shortcuts.h" +#include "inhibit.h" +#include "input-capture.h" +#include "location.h" +#include "memory-monitor.h" +#include "network-monitor.h" +#include "notification.h" +#include "open-uri.h" +#include "permissions.h" +#include "portal-impl.h" +#include "power-profile-monitor.h" +#include "print.h" +#include "proxy-resolver.h" +#include "realtime.h" +#include "remote-desktop.h" +#include "request.h" +#include "screen-cast.h" +#include "screenshot.h" +#include "secret.h" +#include "settings.h" +#include "trash.h" +#include "wallpaper.h" + +static GMainLoop *loop = NULL; + +gboolean opt_verbose; +static gboolean opt_replace; +static gboolean show_version; + +static GOptionEntry entries[] = { + { "verbose", 'v', 0, G_OPTION_ARG_NONE, &opt_verbose, "Print debug information during command processing", NULL }, + { "replace", 'r', 0, G_OPTION_ARG_NONE, &opt_replace, "Replace a running instance", NULL }, + { "version", 0, 0, G_OPTION_ARG_NONE, &show_version, "Show program version.", NULL}, + { NULL } +}; + +static void +message_handler (const gchar *log_domain, + GLogLevelFlags log_level, + const gchar *message, + gpointer user_data) +{ + /* Make this look like normal console output */ + if (log_level & G_LOG_LEVEL_DEBUG) + fprintf (stderr, "XDP: %s\n", message); + else + fprintf (stderr, "%s: %s\n", g_get_prgname (), message); +} + +static void +printerr_handler (const gchar *string) +{ + int is_tty = isatty (1); + const char *prefix = ""; + const char *suffix = ""; + if (is_tty) + { + prefix = "\x1b[31m\x1b[1m"; /* red, bold */ + suffix = "\x1b[22m\x1b[0m"; /* bold off, color reset */ + } + fprintf (stderr, "%serror: %s%s\n", prefix, suffix, string); +} + +static gboolean +method_needs_request (GDBusMethodInvocation *invocation) +{ + const char *interface; + const char *method; + + interface = g_dbus_method_invocation_get_interface_name (invocation); + method = g_dbus_method_invocation_get_method_name (invocation); + + if (strcmp (interface, "org.freedesktop.portal.ScreenCast") == 0) + { + if (strcmp (method, "OpenPipeWireRemote") == 0) + return FALSE; + else + return TRUE; + } + else if (strcmp (interface, "org.freedesktop.portal.RemoteDesktop") == 0) + { + if (strstr (method, "Notify") == method || strcmp (method, "ConnectToEIS") == 0) + return FALSE; + else + return TRUE; + } + else if (strcmp (interface, "org.freedesktop.portal.Clipboard") == 0) + { + return FALSE; + } + else if (strcmp (interface, "org.freedesktop.portal.Camera") == 0) + { + if (strcmp (method, "OpenPipeWireRemote") == 0) + return FALSE; + else + return TRUE; + } + else if (strcmp (interface, "org.freedesktop.portal.DynamicLauncher") == 0) + { + if (strcmp (method, "PrepareInstall") == 0) + return TRUE; + else + return FALSE; + } + else if (strcmp (interface, "org.freedesktop.portal.Background") == 0) + { + if (strcmp (method, "SetStatus") == 0) + return FALSE; + else + return TRUE; + } + else if (strcmp (interface, "org.freedesktop.portal.InputCapture") == 0) + { + if (strcmp (method, "ConnectToEIS") == 0 || + strcmp (method, "Enable") == 0 || + strcmp (method, "Disable") == 0 || + strcmp (method, "Release") == 0) + return FALSE; + else + return TRUE; + } + else + { + return TRUE; + } +} + +static gboolean +authorize_callback (GDBusInterfaceSkeleton *interface, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + g_autoptr(XdpAppInfo) app_info = NULL; + + g_autoptr(GError) error = NULL; + + app_info = xdp_invocation_lookup_app_info_sync (invocation, NULL, &error); + if (app_info == NULL) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Portal operation not allowed: %s", error->message); + return FALSE; + } + + if (method_needs_request (invocation)) + request_init_invocation (invocation, app_info); + else + call_init_invocation (invocation, app_info); + + return TRUE; +} + +static void +export_portal_implementation (GDBusConnection *connection, + GDBusInterfaceSkeleton *skeleton) +{ + g_autoptr(GError) error = NULL; + + if (skeleton == NULL) + { + g_warning ("No skeleton to export"); + return; + } + + g_dbus_interface_skeleton_set_flags (skeleton, + G_DBUS_INTERFACE_SKELETON_FLAGS_HANDLE_METHOD_INVOCATIONS_IN_THREAD); + g_signal_connect (skeleton, "g-authorize-method", + G_CALLBACK (authorize_callback), NULL); + + if (!g_dbus_interface_skeleton_export (skeleton, + connection, + DESKTOP_PORTAL_OBJECT_PATH, + &error)) + { + g_warning ("Error: %s", error->message); + return; + } + + g_debug ("providing portal %s", g_dbus_interface_skeleton_get_info (skeleton)->name); +} + +static void +peer_died_cb (const char *name) +{ + close_requests_for_sender (name); + close_sessions_for_sender (name); +} + +static void +on_bus_acquired (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + PortalImplementation *implementation; + PortalImplementation *lockdown_impl; + PortalImplementation *access_impl; + g_autoptr(GError) error = NULL; + XdpDbusImplLockdown *lockdown; + GQuark portal_errors G_GNUC_UNUSED; + GPtrArray *impls; + + /* make sure errors are registered */ + portal_errors = XDG_DESKTOP_PORTAL_ERROR; + + xdp_connection_track_name_owners (connection, peer_died_cb); + init_document_proxy (connection); + init_permission_store (connection); + + lockdown_impl = find_portal_implementation ("org.freedesktop.impl.portal.Lockdown"); + if (lockdown_impl != NULL) + lockdown = xdp_dbus_impl_lockdown_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_NONE, + lockdown_impl->dbus_name, + DESKTOP_PORTAL_OBJECT_PATH, + NULL, &error); + else + lockdown = xdp_dbus_impl_lockdown_skeleton_new (); + + export_portal_implementation (connection, memory_monitor_create (connection)); + export_portal_implementation (connection, power_profile_monitor_create (connection)); + export_portal_implementation (connection, network_monitor_create (connection)); + export_portal_implementation (connection, proxy_resolver_create (connection)); + export_portal_implementation (connection, trash_create (connection)); + export_portal_implementation (connection, game_mode_create (connection)); + export_portal_implementation (connection, realtime_create (connection)); + + impls = find_all_portal_implementations ("org.freedesktop.impl.portal.Settings"); + export_portal_implementation (connection, settings_create (connection, impls)); + g_ptr_array_free (impls, TRUE); + + implementation = find_portal_implementation ("org.freedesktop.impl.portal.FileChooser"); + if (implementation != NULL) + export_portal_implementation (connection, + file_chooser_create (connection, implementation->dbus_name, lockdown)); + + implementation = find_portal_implementation ("org.freedesktop.impl.portal.AppChooser"); + if (implementation != NULL) + export_portal_implementation (connection, + open_uri_create (connection, implementation->dbus_name, lockdown)); + + implementation = find_portal_implementation ("org.freedesktop.impl.portal.Print"); + if (implementation != NULL) + export_portal_implementation (connection, + print_create (connection, implementation->dbus_name, lockdown)); + + implementation = find_portal_implementation ("org.freedesktop.impl.portal.Notification"); + if (implementation != NULL) + export_portal_implementation (connection, + notification_create (connection, implementation->dbus_name)); + + implementation = find_portal_implementation ("org.freedesktop.impl.portal.Inhibit"); + if (implementation != NULL) + export_portal_implementation (connection, + inhibit_create (connection, implementation->dbus_name)); + + access_impl = find_portal_implementation ("org.freedesktop.impl.portal.Access"); + if (access_impl != NULL) + { + PortalImplementation *tmp; + + export_portal_implementation (connection, + device_create (connection, + access_impl->dbus_name, + lockdown)); +#ifdef HAVE_GEOCLUE + export_portal_implementation (connection, + location_create (connection, + access_impl->dbus_name, + lockdown)); +#endif + + export_portal_implementation (connection, + camera_create (connection, lockdown)); + + tmp = find_portal_implementation ("org.freedesktop.impl.portal.Screenshot"); + if (tmp != NULL) + export_portal_implementation (connection, + screenshot_create (connection, + access_impl->dbus_name, + tmp->dbus_name)); + + tmp = find_portal_implementation ("org.freedesktop.impl.portal.Background"); + if (tmp != NULL) + export_portal_implementation (connection, + background_create (connection, + access_impl->dbus_name, + tmp->dbus_name)); + + tmp = find_portal_implementation ("org.freedesktop.impl.portal.Wallpaper"); + if (tmp != NULL) + export_portal_implementation (connection, + wallpaper_create (connection, + access_impl->dbus_name, + tmp->dbus_name)); + } + + implementation = find_portal_implementation ("org.freedesktop.impl.portal.Account"); + if (implementation != NULL) + export_portal_implementation (connection, + account_create (connection, implementation->dbus_name)); + + implementation = find_portal_implementation ("org.freedesktop.impl.portal.Email"); + if (implementation != NULL) + export_portal_implementation (connection, + email_create (connection, implementation->dbus_name)); + + implementation = find_portal_implementation ("org.freedesktop.impl.portal.Secret"); + if (implementation != NULL) + export_portal_implementation (connection, + secret_create (connection, implementation->dbus_name)); + + implementation = find_portal_implementation ("org.freedesktop.impl.portal.GlobalShortcuts"); + if (implementation != NULL) + export_portal_implementation (connection, + global_shortcuts_create (connection, implementation->dbus_name)); + + implementation = find_portal_implementation ("org.freedesktop.impl.portal.DynamicLauncher"); + if (implementation != NULL) + export_portal_implementation (connection, + dynamic_launcher_create (connection, implementation->dbus_name)); + + implementation = find_portal_implementation ("org.freedesktop.impl.portal.ScreenCast"); + if (implementation != NULL) + export_portal_implementation (connection, + screen_cast_create (connection, implementation->dbus_name)); + + implementation = find_portal_implementation ("org.freedesktop.impl.portal.RemoteDesktop"); + if (implementation != NULL) + export_portal_implementation (connection, + remote_desktop_create (connection, implementation->dbus_name)); + + implementation = find_portal_implementation ("org.freedesktop.impl.portal.Clipboard"); + if (implementation != NULL) + export_portal_implementation ( + connection, clipboard_create (connection, implementation->dbus_name)); + + implementation = find_portal_implementation ("org.freedesktop.impl.portal.InputCapture"); + if (implementation != NULL) + export_portal_implementation (connection, + input_capture_create (connection, implementation->dbus_name)); +} + +static void +on_name_acquired (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + g_debug ("%s acquired", name); +} + +static void +on_name_lost (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + g_main_loop_quit (loop); +} + +int +main (int argc, char *argv[]) +{ + guint owner_id; + g_autoptr(GError) error = NULL; + g_autoptr(GDBusConnection) session_bus = NULL; + g_autoptr(GOptionContext) context; + + setlocale (LC_ALL, ""); + bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); + bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); + textdomain (GETTEXT_PACKAGE); + + /* Note: if you add any more environment variables here, update + * handle_launch() in dynamic-launcher.c to unset them before launching apps + */ + /* Avoid even loading gvfs to avoid accidental confusion */ + g_setenv ("GIO_USE_VFS", "local", TRUE); + + /* Avoid pointless and confusing recursion */ + g_unsetenv ("GTK_USE_PORTAL"); + + context = g_option_context_new ("- desktop portal"); + g_option_context_set_summary (context, + "A portal service for flatpak and other desktop containment frameworks."); + g_option_context_set_description (context, + "xdg-desktop-portal works by exposing D-Bus interfaces known as portals\n" + "under the well-known name org.freedesktop.portal.Desktop and object\n" + "path /org/freedesktop/portal/desktop.\n" + "\n" + "Documentation for the available D-Bus interfaces can be found at\n" + "https://flatpak.github.io/xdg-desktop-portal/portal-docs.html\n" + "\n" + "Please report issues at https://github.com/flatpak/xdg-desktop-portal/issues"); + g_option_context_add_main_entries (context, entries, NULL); + if (!g_option_context_parse (context, &argc, &argv, &error)) + { + g_printerr ("%s: %s", g_get_application_name (), error->message); + g_printerr ("\n"); + g_printerr ("Try \"%s --help\" for more information.", + g_get_prgname ()); + g_printerr ("\n"); + return 1; + } + + if (show_version) + { + g_print (PACKAGE_STRING "\n"); + return 0; + } + + g_set_printerr_handler (printerr_handler); + + if (opt_verbose) + g_log_set_handler (G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, message_handler, NULL); + + g_set_prgname (argv[0]); + + load_portal_configuration (opt_verbose); + load_installed_portals (opt_verbose); + + loop = g_main_loop_new (NULL, FALSE); + + session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); + if (session_bus == NULL) + { + g_printerr ("No session bus: %s", error->message); + return 2; + } + + owner_id = g_bus_own_name (G_BUS_TYPE_SESSION, + "org.freedesktop.portal.Desktop", + G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | (opt_replace ? G_BUS_NAME_OWNER_FLAGS_REPLACE : 0), + on_bus_acquired, + on_name_acquired, + on_name_lost, + NULL, + NULL); + + g_main_loop_run (loop); + + g_bus_unown_name (owner_id); + g_main_loop_unref (loop); + + return 0; +} diff --git a/src/xdg-desktop-portal.gresource.xml b/src/xdg-desktop-portal.gresource.xml new file mode 100644 index 0000000..aa1c434 --- /dev/null +++ b/src/xdg-desktop-portal.gresource.xml @@ -0,0 +1,8 @@ + + + + data/org.freedesktop.portal.FileChooser.xml + data/org.freedesktop.portal.OpenURI.xml + data/org.freedesktop.portal.Print.xml + + diff --git a/src/xdg-desktop-portal.service.in b/src/xdg-desktop-portal.service.in new file mode 100644 index 0000000..1f8a455 --- /dev/null +++ b/src/xdg-desktop-portal.service.in @@ -0,0 +1,9 @@ +[Unit] +Description=Portal service +PartOf=graphical-session.target + +[Service] +Type=dbus +BusName=org.freedesktop.portal.Desktop +ExecStart=@libexecdir@/xdg-desktop-portal +Slice=session.slice diff --git a/src/xdp-utils.c b/src/xdp-utils.c new file mode 100644 index 0000000..840f716 --- /dev/null +++ b/src/xdp-utils.c @@ -0,0 +1,2409 @@ +/* + * Copyright © 2014 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Alexander Larsson + */ + +#include "config.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#ifdef HAVE_SYS_VFS_H +#include +#endif +#ifdef HAVE_SYS_MOUNT_H +#include +#endif + +#ifdef HAVE_LIBSYSTEMD +#include +#include "sd-escape.h" +#endif + +#include +#include +#include + +#include "xdp-utils.h" + +#define DBUS_NAME_DBUS "org.freedesktop.DBus" +#define DBUS_INTERFACE_DBUS DBUS_NAME_DBUS +#define DBUS_PATH_DBUS "/org/freedesktop/DBus" + +G_LOCK_DEFINE (app_infos); +static GHashTable *app_info_by_unique_name; + +/* Based on g_mkstemp from glib */ + +gint +xdp_mkstempat (int dir_fd, + gchar *tmpl, + int flags, + int mode) +{ + char *XXXXXX; + int count, fd; + static const char letters[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + static const int NLETTERS = sizeof (letters) - 1; + gint64 value; + gint64 current_time; + static int counter = 0; + + g_return_val_if_fail (tmpl != NULL, -1); + + /* find the last occurrence of "XXXXXX" */ + XXXXXX = g_strrstr (tmpl, "XXXXXX"); + + if (!XXXXXX || strncmp (XXXXXX, "XXXXXX", 6)) + { + errno = EINVAL; + return -1; + } + + /* Get some more or less random data. */ + current_time = g_get_real_time (); + value = ((current_time % G_USEC_PER_SEC) ^ (current_time / G_USEC_PER_SEC)) + counter++; + + for (count = 0; count < 100; value += 7777, ++count) + { + gint64 v = value; + + /* Fill in the random bits. */ + XXXXXX[0] = letters[v % NLETTERS]; + v /= NLETTERS; + XXXXXX[1] = letters[v % NLETTERS]; + v /= NLETTERS; + XXXXXX[2] = letters[v % NLETTERS]; + v /= NLETTERS; + XXXXXX[3] = letters[v % NLETTERS]; + v /= NLETTERS; + XXXXXX[4] = letters[v % NLETTERS]; + v /= NLETTERS; + XXXXXX[5] = letters[v % NLETTERS]; + + fd = openat (dir_fd, tmpl, flags | O_CREAT | O_EXCL, mode); + + if (fd >= 0) + return fd; + else if (errno != EEXIST) + /* Any other error will apply also to other names we might + * try, and there are 2^32 or so of them, so give up now. + */ + return -1; + } + + /* We got out of the loop because we ran out of combinations to try. */ + errno = EEXIST; + return -1; +} + +struct _XdpAppInfo { + volatile gint ref_count; + char *id; + XdpAppInfoKind kind; + + union + { + struct + { + GKeyFile *keyfile; + /* pid namespace mapping */ + GMutex pidns_lock; + ino_t pidns_id; + } flatpak; + struct + { + GKeyFile *keyfile; + } snap; + } u; +}; + +static XdpAppInfo * +xdp_app_info_new (XdpAppInfoKind kind) +{ + XdpAppInfo *app_info = g_new0 (XdpAppInfo, 1); + app_info->ref_count = 1; + app_info->kind = kind; + return app_info; +} + +#ifdef HAVE_LIBSYSTEMD +char * +_xdp_parse_app_id_from_unit_name (const char *unit) +{ + g_autoptr(GRegex) regex1 = NULL; + g_autoptr(GRegex) regex2 = NULL; + g_autoptr(GMatchInfo) match = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *app_id = NULL; + + g_assert (g_str_has_prefix (unit, "app-")); + + /* + * From https://systemd.io/DESKTOP_ENVIRONMENTS/ the format is one of: + * app[-]--.scope + * app[-]--.slice + */ + regex1 = g_regex_new ("^app-(?:[[:alnum:]]+\\-)?(.+?)(?:\\-[[:alnum:]]*)(?:\\.scope|\\.slice)$", 0, 0, &error); + g_assert (error == NULL); + /* + * app[-]--autostart.service -> no longer true since systemd v248 + * app[-]-[@].service + */ + regex2 = g_regex_new ("^app-(?:[[:alnum:]]+\\-)?(.+?)(?:@[[:alnum:]]*|\\-autostart)?\\.service$", 0, 0, &error); + g_assert (error == NULL); + + if (!g_regex_match (regex1, unit, 0, &match)) + g_clear_pointer (&match, g_match_info_unref); + + if (match == NULL && !g_regex_match (regex2, unit, 0, &match)) + g_clear_pointer (&match, g_match_info_unref); + + if (match != NULL) + { + const char *escaped_app_id; + /* Unescape the unit name which may have \x hex codes in it, e.g. + * "app-gnome-org.gnome.Evolution\x2dalarm\x2dnotify-2437.scope" + */ + escaped_app_id = g_match_info_fetch (match, 1); + if (cunescape (escaped_app_id, UNESCAPE_RELAX, &app_id) < 0) + app_id = g_strdup (""); + } + else + { + app_id = g_strdup (""); + } + + return g_steal_pointer (&app_id); +} +#endif /* HAVE_LIBSYSTEMD */ + +void +set_appid_from_pid (XdpAppInfo *app_info, pid_t pid) +{ +#ifdef HAVE_LIBSYSTEMD + g_autofree char *unit = NULL; + int res; + + g_return_if_fail (app_info->id == NULL); + + res = sd_pid_get_user_unit (pid, &unit); + /* + * The session might not be managed by systemd or there could be an error + * fetching our own systemd units or the unit might not be started by the + * desktop environment (e.g. it's a script run from terminal). + */ + if (res == -ENODATA || res < 0 || !unit || !g_str_has_prefix (unit, "app-")) + { + app_info->id = g_strdup (""); + return; + } + + app_info->id = _xdp_parse_app_id_from_unit_name (unit); + g_debug ("Assigning app ID \"%s\" to pid %ld which has unit \"%s\"", + app_info->id, (long) pid, unit); + +#else + app_info->id = g_strdup (""); +#endif /* HAVE_LIBSYSTEMD */ +} + +static XdpAppInfo * +xdp_app_info_new_host (pid_t pid) +{ + XdpAppInfo *app_info = xdp_app_info_new (XDP_APP_INFO_KIND_HOST); + set_appid_from_pid (app_info, pid); + return app_info; +} + +static void +xdp_app_info_free (XdpAppInfo *app_info) +{ + g_free (app_info->id); + + switch (app_info->kind) + { + case XDP_APP_INFO_KIND_FLATPAK: + g_clear_pointer (&app_info->u.flatpak.keyfile, g_key_file_free); + break; + + case XDP_APP_INFO_KIND_SNAP: + g_clear_pointer (&app_info->u.snap.keyfile, g_key_file_free); + break; + + case XDP_APP_INFO_KIND_HOST: + default: + break; + } + + g_free (app_info); +} + +XdpAppInfo * +xdp_app_info_ref (XdpAppInfo *app_info) +{ + g_return_val_if_fail (app_info != NULL, NULL); + + g_atomic_int_inc (&app_info->ref_count); + return app_info; +} + +void +xdp_app_info_unref (XdpAppInfo *app_info) +{ + g_return_if_fail (app_info != NULL); + + if (g_atomic_int_dec_and_test (&app_info->ref_count)) + xdp_app_info_free (app_info); +} + +const char * +xdp_app_info_get_id (XdpAppInfo *app_info) +{ + g_return_val_if_fail (app_info != NULL, NULL); + + return app_info->id; +} + +XdpAppInfoKind +xdp_app_info_get_kind (XdpAppInfo *app_info) +{ + g_return_val_if_fail (app_info != NULL, -1); + + return app_info->kind; +} + +GAppInfo * +xdp_app_info_load_app_info (XdpAppInfo *app_info) +{ + g_autofree char *desktop_id = NULL; + + g_return_val_if_fail (app_info != NULL, NULL); + + switch (app_info->kind) + { + case XDP_APP_INFO_KIND_FLATPAK: + desktop_id = g_strconcat (app_info->id, ".desktop", NULL); + break; + + case XDP_APP_INFO_KIND_SNAP: + desktop_id = g_key_file_get_string (app_info->u.snap.keyfile, + SNAP_METADATA_GROUP_INFO, + SNAP_METADATA_KEY_DESKTOP_FILE, + NULL); + break; + + case XDP_APP_INFO_KIND_HOST: + default: + desktop_id = NULL; + break; + } + + if (desktop_id == NULL) + return NULL; + + return G_APP_INFO (g_desktop_app_info_new (desktop_id)); +} + +static gboolean +needs_quoting (const char *arg) +{ + while (*arg != 0) + { + char c = *arg; + if (!g_ascii_isalnum (c) && + !(c == '-' || c == '/' || c == '~' || + c == ':' || c == '.' || c == '_' || + c == '=' || c == '@')) + return TRUE; + arg++; + } + return FALSE; +} + +static char * +maybe_quote (const char *arg, + gboolean quote_escape) +{ + if (!quote_escape || !needs_quoting (arg)) + return g_strdup (arg); + else + return g_shell_quote (arg); +} + +char ** +xdp_app_info_rewrite_commandline (XdpAppInfo *app_info, + const char * const *commandline, + gboolean quote_escape) +{ + g_autoptr(GPtrArray) args = NULL; + + g_return_val_if_fail (app_info != NULL, NULL); + + if (app_info->kind == XDP_APP_INFO_KIND_HOST) + { + int i; + args = g_ptr_array_new_with_free_func (g_free); + for (i = 0; commandline && commandline[i]; i++) + g_ptr_array_add (args, maybe_quote (commandline[i], quote_escape)); + g_ptr_array_add (args, NULL); + return (char **)g_ptr_array_free (g_steal_pointer (&args), FALSE); + } + else if (app_info->kind == XDP_APP_INFO_KIND_FLATPAK) + { + args = g_ptr_array_new_with_free_func (g_free); + + g_ptr_array_add (args, g_strdup ("flatpak")); + g_ptr_array_add (args, g_strdup ("run")); + if (commandline && commandline[0]) + { + int i; + g_autofree char *quoted_command = NULL; + + quoted_command = maybe_quote (commandline[0], quote_escape); + + g_ptr_array_add (args, g_strdup_printf ("--command=%s", quoted_command)); + + /* Always quote the app ID if quote_escape is enabled to make + * rewriting the file simpler in case the app is renamed. + */ + if (quote_escape) + g_ptr_array_add (args, g_shell_quote (app_info->id)); + else + g_ptr_array_add (args, g_strdup (app_info->id)); + + for (i = 1; commandline[i]; i++) + g_ptr_array_add (args, maybe_quote (commandline[i], quote_escape)); + } + else + g_ptr_array_add (args, g_shell_quote (app_info->id)); + g_ptr_array_add (args, NULL); + + return (char **)g_ptr_array_free (g_steal_pointer (&args), FALSE); + } + else + return NULL; +} + +char * +xdp_app_info_get_tryexec_path (XdpAppInfo *app_info) +{ + g_return_val_if_fail (app_info != NULL, NULL); + + if (app_info->kind == XDP_APP_INFO_KIND_FLATPAK) + { + g_autofree char *original_app_path = NULL; + g_autofree char *tryexec_path = NULL; + g_autofree char *app_slash = NULL; + g_autofree char *app_path = NULL; + char *app_slash_pointer; + char *path; + + original_app_path = g_key_file_get_string (app_info->u.flatpak.keyfile, + FLATPAK_METADATA_GROUP_INSTANCE, + FLATPAK_METADATA_KEY_ORIGINAL_APP_PATH, NULL); + app_path = g_key_file_get_string (app_info->u.flatpak.keyfile, + FLATPAK_METADATA_GROUP_INSTANCE, + FLATPAK_METADATA_KEY_APP_PATH, NULL); + path = original_app_path ? original_app_path : app_path; + + if (path == NULL || *path == '\0') + return NULL; + + app_slash = g_strconcat ("app/", app_info->id, NULL); + + app_slash_pointer = strstr (path, app_slash); + if (app_slash_pointer == NULL) + return NULL; + + /* Terminate path after the flatpak installation path such as .local/share/flatpak/ */ + *app_slash_pointer = '\0'; + + /* Find the path to the wrapper script exported by Flatpak, which can be + * used in a desktop file's TryExec= + */ + tryexec_path = g_strconcat (path, "exports/bin/", app_info->id, NULL); + if (access (tryexec_path, X_OK) != 0) + { + g_debug ("Wrapper script unexpectedly not executable or nonexistent: %s", tryexec_path); + return NULL; + } + + return g_steal_pointer (&tryexec_path); + } + else + return NULL; +} + +char * +xdp_app_info_get_instance (XdpAppInfo *app_info) +{ + g_return_val_if_fail (app_info != NULL, NULL); + + if (app_info->kind != XDP_APP_INFO_KIND_FLATPAK) + return NULL; + + return g_key_file_get_string (app_info->u.flatpak.keyfile, + FLATPAK_METADATA_GROUP_INSTANCE, + FLATPAK_METADATA_KEY_INSTANCE_ID, + NULL); +} + +gboolean +xdp_app_info_is_host (XdpAppInfo *app_info) +{ + g_return_val_if_fail (app_info != NULL, FALSE); + + return app_info->kind == XDP_APP_INFO_KIND_HOST; +} + +gboolean +xdp_app_info_supports_opath (XdpAppInfo *app_info) +{ + return + app_info->kind == XDP_APP_INFO_KIND_FLATPAK || + app_info->kind == XDP_APP_INFO_KIND_HOST; +} + +char * +xdp_app_info_remap_path (XdpAppInfo *app_info, + const char *path) +{ + if (app_info->kind == XDP_APP_INFO_KIND_FLATPAK) + { + g_autofree char *app_path = g_key_file_get_string (app_info->u.flatpak.keyfile, + FLATPAK_METADATA_GROUP_INSTANCE, + FLATPAK_METADATA_KEY_APP_PATH, NULL); + g_autofree char *runtime_path = g_key_file_get_string (app_info->u.flatpak.keyfile, + FLATPAK_METADATA_GROUP_INSTANCE, + FLATPAK_METADATA_KEY_RUNTIME_PATH, + NULL); + + /* For apps we translate /app and /usr to the installed locations. + Also, we need to rewrite to drop the /newroot prefix added by + bubblewrap for other files to work. See + https://github.com/projectatomic/bubblewrap/pull/172 + for a bit more information on the /newroot issue. + */ + + if (g_str_has_prefix (path, "/newroot/")) + path = path + strlen ("/newroot"); + + if (app_path != NULL && g_str_has_prefix (path, "/app/")) + return g_build_filename (app_path, path + strlen ("/app/"), NULL); + else if (runtime_path != NULL && g_str_has_prefix (path, "/usr/")) + return g_build_filename (runtime_path, path + strlen ("/usr/"), NULL); + else if (g_str_has_prefix (path, "/run/host/usr/")) + return g_build_filename ("/usr", path + strlen ("/run/host/usr/"), NULL); + else if (g_str_has_prefix (path, "/run/host/etc/")) + return g_build_filename ("/etc", path + strlen ("/run/host/etc/"), NULL); + else if (g_str_has_prefix (path, "/run/flatpak/app/")) + return g_build_filename (g_get_user_runtime_dir (), "app", + path + strlen ("/run/flatpak/app/"), NULL); + else if (g_str_has_prefix (path, "/run/flatpak/doc/")) + return g_build_filename (g_get_user_runtime_dir (), "doc", + path + strlen ("/run/flatpak/doc/"), NULL); + else if (g_str_has_prefix (path, "/var/config/")) + return g_build_filename (g_get_home_dir (), ".var", "app", + app_info->id, "config", + path + strlen ("/var/config/"), NULL); + else if (g_str_has_prefix (path, "/var/data/")) + return g_build_filename (g_get_home_dir (), ".var", "app", + app_info->id, "data", + path + strlen ("/var/data/"), NULL); + } + + return g_strdup (path); +} + +gboolean +xdp_app_info_has_network (XdpAppInfo *app_info) +{ + gboolean has_network; + + switch (app_info->kind) + { + case XDP_APP_INFO_KIND_FLATPAK: + { + g_auto(GStrv) shared = g_key_file_get_string_list (app_info->u.flatpak.keyfile, + "Context", "shared", + NULL, NULL); + if (shared) + has_network = g_strv_contains ((const char * const *)shared, "network"); + else + has_network = FALSE; + } + break; + + case XDP_APP_INFO_KIND_SNAP: + has_network = g_key_file_get_boolean (app_info->u.snap.keyfile, + SNAP_METADATA_GROUP_INFO, + SNAP_METADATA_KEY_NETWORK, NULL); + break; + + case XDP_APP_INFO_KIND_HOST: + default: + has_network = TRUE; + break; + } + + return has_network; +} + +static void +ensure_app_info_by_unique_name (void) +{ + if (app_info_by_unique_name == NULL) + app_info_by_unique_name = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, + (GDestroyNotify)xdp_app_info_unref); +} + +/* Returns NULL with error set on failure, NULL with no error set if not a flatpak, and app-info otherwise */ +static XdpAppInfo * +parse_app_info_from_flatpak_info (int pid, GError **error) +{ + g_autofree char *root_path = NULL; + int root_fd = -1; + int info_fd = -1; + struct stat stat_buf; + g_autoptr(GError) local_error = NULL; + g_autoptr(GMappedFile) mapped = NULL; + g_autoptr(GKeyFile) metadata = NULL; + g_autoptr(XdpAppInfo) app_info = NULL; + const char *group; + g_autofree char *id = NULL; + + root_path = g_strdup_printf ("/proc/%u/root", pid); + root_fd = openat (AT_FDCWD, root_path, O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC | O_NOCTTY); + if (root_fd == -1) + { + if (errno == EACCES) + { + struct statfs buf; + + /* Access to the root dir isn't allowed. This can happen if the root is on a fuse + * filesystem, such as in a toolbox container. We will never have a fuse rootfs + * in the flatpak case, so in that case its safe to ignore this and + * continue to detect other types of apps. + */ + if (statfs (root_path, &buf) == 0 && + buf.f_type == 0x65735546) /* FUSE_SUPER_MAGIC */ + return NULL; + } + + /* Otherwise, we should be able to open the root dir. Probably the app died and + we're failing due to /proc/$pid not existing. In that case fail instead + of treating this as privileged. */ + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Unable to open %s", root_path); + return NULL; + } + + metadata = g_key_file_new (); + + info_fd = openat (root_fd, ".flatpak-info", O_RDONLY | O_CLOEXEC | O_NOCTTY); + close (root_fd); + if (info_fd == -1) + { + if (errno == ENOENT) + { + /* No file => on the host, return NULL with no error */ + return NULL; + } + + /* Some weird error => failure */ + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Unable to open application info file"); + return NULL; + } + + if (fstat (info_fd, &stat_buf) != 0 || !S_ISREG (stat_buf.st_mode)) + { + /* Some weird fd => failure */ + close (info_fd); + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Unable to open application info file"); + return NULL; + } + + mapped = g_mapped_file_new_from_fd (info_fd, FALSE, &local_error); + if (mapped == NULL) + { + close (info_fd); + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Can't map .flatpak-info file: %s", local_error->message); + return NULL; + } + + if (!g_key_file_load_from_data (metadata, + g_mapped_file_get_contents (mapped), + g_mapped_file_get_length (mapped), + G_KEY_FILE_NONE, &local_error)) + { + close (info_fd); + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Can't load .flatpak-info file: %s", local_error->message); + return NULL; + } + + group = "Application"; + if (g_key_file_has_group (metadata, "Runtime")) + group = "Runtime"; + + id = g_key_file_get_string (metadata, group, "name", error); + if (id == NULL || !xdp_is_valid_app_id (id)) + { + close (info_fd); + return NULL; + } + + close (info_fd); + + app_info = xdp_app_info_new (XDP_APP_INFO_KIND_FLATPAK); + app_info->id = g_steal_pointer (&id); + app_info->u.flatpak.keyfile = g_steal_pointer (&metadata); + + return g_steal_pointer (&app_info); +} + +int +_xdp_parse_cgroup_file (FILE *f, gboolean *is_snap) +{ + ssize_t n; + g_autofree char *id = NULL; + g_autofree char *controller = NULL; + g_autofree char *cgroup = NULL; + size_t id_len = 0, controller_len = 0, cgroup_len = 0; + + g_return_val_if_fail(f != NULL, -1); + g_return_val_if_fail(is_snap != NULL, -1); + + *is_snap = FALSE; + do + { + n = getdelim (&id, &id_len, ':', f); + if (n == -1) break; + n = getdelim (&controller, &controller_len, ':', f); + if (n == -1) break; + n = getdelim (&cgroup, &cgroup_len, '\n', f); + if (n == -1) break; + + /* Only consider the freezer, systemd group or unified cgroup + * hierarchies */ + if ((strcmp (controller, "freezer:") == 0 || + strcmp (controller, "name=systemd:") == 0 || + strcmp (controller, ":") == 0) && + strstr (cgroup, "/snap.") != NULL) + { + *is_snap = TRUE; + break; + } + } + while (n >= 0); + + if (n < 0 && !feof(f)) return -1; + + return 0; +} + +static gboolean +pid_is_snap (pid_t pid, GError **error) +{ + g_autofree char *cgroup_path = NULL;; + int fd; + FILE *f = NULL; + gboolean is_snap = FALSE; + int err = 0; + + g_return_val_if_fail(pid > 0, FALSE); + + cgroup_path = g_strdup_printf ("/proc/%u/cgroup", (guint) pid); + fd = open (cgroup_path, O_RDONLY | O_CLOEXEC | O_NOCTTY); + if (fd == -1) + { + err = errno; + goto end; + } + + f = fdopen (fd, "r"); + if (f == NULL) + { + err = errno; + goto end; + } + + fd = -1; /* fd is now owned by f */ + + if (_xdp_parse_cgroup_file (f, &is_snap) == -1) + err = errno; + + fclose (f); + +end: + /* Silence ENOENT, treating it as "not a snap" */ + if (err != 0 && err != ENOENT) + { + g_set_error (error, G_IO_ERROR, g_io_error_from_errno (err), + "Could not parse cgroup info for pid %u: %s", (guint) pid, + g_strerror (err)); + } + return is_snap; +} + +/* Returns NULL with error set on failure, NULL with no error set if not a snap, and app-info otherwise */ +static XdpAppInfo * +parse_app_info_from_snap (pid_t pid, GError **error) +{ + g_autoptr(GError) local_error = NULL; + g_autofree char *pid_str = NULL; + const char *argv[] = { "snap", "routine", "portal-info", NULL, NULL }; + g_autofree char *output = NULL; + g_autoptr(GKeyFile) metadata = NULL; + g_autoptr(XdpAppInfo) app_info = NULL; + g_autofree char *snap_name = NULL; + + /* Check the process's cgroup membership to fail quickly for non-snaps */ + if (!pid_is_snap (pid, error)) return NULL; + + pid_str = g_strdup_printf ("%u", (guint) pid); + argv[3] = pid_str; + if (!xdp_spawnv (NULL, &output, 0, error, argv)) + { + return NULL; + } + + metadata = g_key_file_new (); + if (!g_key_file_load_from_data (metadata, output, -1, G_KEY_FILE_NONE, &local_error)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Can't read snap info for pid %u: %s", pid, local_error->message); + return NULL; + } + + snap_name = g_key_file_get_string (metadata, SNAP_METADATA_GROUP_INFO, + SNAP_METADATA_KEY_INSTANCE_NAME, error); + if (snap_name == NULL) + { + return NULL; + } + + app_info = xdp_app_info_new (XDP_APP_INFO_KIND_SNAP); + app_info->id = g_strconcat ("snap.", snap_name, NULL); + app_info->u.snap.keyfile = g_steal_pointer (&metadata); + + return g_steal_pointer (&app_info); +} + + +XdpAppInfo * +xdp_get_app_info_from_pid (pid_t pid, + GError **error) +{ + g_autoptr(XdpAppInfo) app_info = NULL; + g_autoptr(GError) local_error = NULL; + + app_info = parse_app_info_from_flatpak_info (pid, &local_error); + if (app_info == NULL && local_error) + { + g_propagate_error (error, g_steal_pointer (&local_error)); + return NULL; + } + + if (app_info == NULL) + { + app_info = parse_app_info_from_snap (pid, &local_error); + if (app_info == NULL && local_error) + { + g_propagate_error (error, g_steal_pointer (&local_error)); + return NULL; + } + } + + if (app_info == NULL) + app_info = xdp_app_info_new_host (pid); + + return g_steal_pointer (&app_info); +} + +static XdpAppInfo * +lookup_cached_app_info_by_sender (const char *sender) +{ + XdpAppInfo *app_info = NULL; + + G_LOCK (app_infos); + if (app_info_by_unique_name) + { + app_info = g_hash_table_lookup (app_info_by_unique_name, sender); + if (app_info) + xdp_app_info_ref (app_info); + } + G_UNLOCK (app_infos); + + return app_info; +} + +static XdpAppInfo * +xdp_connection_lookup_app_info_sync (GDBusConnection *connection, + const char *sender, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GDBusMessage) msg = NULL; + g_autoptr(GDBusMessage) reply = NULL; + g_autoptr(XdpAppInfo) app_info = NULL; + GVariant *body; + guint32 pid = 0; + + app_info = lookup_cached_app_info_by_sender (sender); + if (app_info) + return g_steal_pointer (&app_info); + + msg = g_dbus_message_new_method_call (DBUS_NAME_DBUS, + DBUS_PATH_DBUS, + DBUS_INTERFACE_DBUS, + "GetConnectionUnixProcessID"); + g_dbus_message_set_body (msg, g_variant_new ("(s)", sender)); + + reply = g_dbus_connection_send_message_with_reply_sync (connection, msg, + G_DBUS_SEND_MESSAGE_FLAGS_NONE, + 30000, + NULL, + cancellable, + error); + if (reply == NULL) + return NULL; + + if (g_dbus_message_get_message_type (reply) == G_DBUS_MESSAGE_TYPE_ERROR) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Can't find peer app id"); + return NULL; + } + + body = g_dbus_message_get_body (reply); + g_variant_get (body, "(u)", &pid); + + app_info = xdp_get_app_info_from_pid (pid, error); + if (app_info == NULL) + return NULL; + + G_LOCK (app_infos); + ensure_app_info_by_unique_name (); + g_hash_table_insert (app_info_by_unique_name, g_strdup (sender), + xdp_app_info_ref (app_info)); + G_UNLOCK (app_infos); + + return g_steal_pointer (&app_info); +} + +XdpAppInfo * +xdp_invocation_lookup_app_info_sync (GDBusMethodInvocation *invocation, + GCancellable *cancellable, + GError **error) +{ + GDBusConnection *connection = g_dbus_method_invocation_get_connection (invocation); + const gchar *sender = g_dbus_method_invocation_get_sender (invocation); + + return xdp_connection_lookup_app_info_sync (connection, sender, cancellable, error); +} + +static void +name_owner_changed (GDBusConnection *connection, + const gchar *sender_name, + const gchar *object_path, + const gchar *interface_name, + const gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + const char *name, *from, *to; + XdpPeerDiedCallback peer_died_cb = user_data; + + g_variant_get (parameters, "(&s&s&s)", &name, &from, &to); + + if (name[0] == ':' && + strcmp (name, from) == 0 && + strcmp (to, "") == 0) + { + G_LOCK (app_infos); + if (app_info_by_unique_name) + g_hash_table_remove (app_info_by_unique_name, name); + G_UNLOCK (app_infos); + + if (peer_died_cb) + peer_died_cb (name); + } +} + +void +xdp_connection_track_name_owners (GDBusConnection *connection, + XdpPeerDiedCallback peer_died_cb) +{ + g_dbus_connection_signal_subscribe (connection, + DBUS_NAME_DBUS, + DBUS_INTERFACE_DBUS, + "NameOwnerChanged", + DBUS_PATH_DBUS, + NULL, + G_DBUS_SIGNAL_FLAGS_NONE, + name_owner_changed, + peer_died_cb, NULL); +} + +gboolean +xdp_filter_options (GVariant *options, + GVariantBuilder *filtered, + XdpOptionKey *supported_options, + int n_supported_options, + GError **error) +{ + int i; + gboolean ret = TRUE; + + for (i = 0; i < n_supported_options; i++) + { + g_autoptr(GVariant) value = NULL; + + value = g_variant_lookup_value (options, + supported_options[i].key, + supported_options[i].type); + if (!value) + { + value = g_variant_lookup_value (options, supported_options[i].key, NULL); + if (value) + { + if (*error == NULL) + g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Expected type '%s' for option '%s', got '%s'", + g_variant_type_peek_string (supported_options[i].type), + supported_options[i].key, + g_variant_type_peek_string (g_variant_get_type (value))); + ret = FALSE; + } + + continue; + } + + if (supported_options[i].validate) + { + g_autoptr(GError) local_error = NULL; + + if (!supported_options[i].validate (supported_options[i].key, value, options, &local_error)) + { + if (ret) + { + ret = FALSE; + if (error && *error == NULL) + { + g_propagate_error (error, local_error); + local_error = NULL; + } + } + + continue; + } + } + + g_variant_builder_add (filtered, "{sv}", supported_options[i].key, g_steal_pointer (&value)); + } + + return ret; +} + +static const GDBusErrorEntry xdg_desktop_portal_error_entries[] = { + { XDG_DESKTOP_PORTAL_ERROR_FAILED, "org.freedesktop.portal.Error.Failed" }, + { XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, "org.freedesktop.portal.Error.InvalidArgument" }, + { XDG_DESKTOP_PORTAL_ERROR_NOT_FOUND, "org.freedesktop.portal.Error.NotFound" }, + { XDG_DESKTOP_PORTAL_ERROR_EXISTS, "org.freedesktop.portal.Error.Exists" }, + { XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, "org.freedesktop.portal.Error.NotAllowed" }, + { XDG_DESKTOP_PORTAL_ERROR_CANCELLED, "org.freedesktop.portal.Error.Cancelled" }, + { XDG_DESKTOP_PORTAL_ERROR_WINDOW_DESTROYED, "org.freedesktop.portal.Error.WindowDestroyed" } +}; + +GQuark +xdg_desktop_portal_error_quark (void) +{ + static volatile gsize quark_volatile = 0; + + g_dbus_error_register_error_domain ("xdg-desktop-portal-error-quark", + &quark_volatile, + xdg_desktop_portal_error_entries, + G_N_ELEMENTS (xdg_desktop_portal_error_entries)); + return (GQuark) quark_volatile; +} + +static char * +verify_proc_self_fd (XdpAppInfo *app_info, + const char *proc_path, + GError **error) +{ + char path_buffer[PATH_MAX + 1]; + ssize_t symlink_size; + int saved_errno; + + symlink_size = readlink (proc_path, path_buffer, PATH_MAX); + if (symlink_size < 0) + { + saved_errno = errno; + g_set_error (error, G_IO_ERROR, g_io_error_from_errno (saved_errno), + "readlink %s: %s", proc_path, g_strerror (saved_errno)); + return NULL; + } + + path_buffer[symlink_size] = 0; + + /* All normal paths start with /, but some weird things + don't, such as socket:[27345] or anon_inode:[eventfd]. + We don't support any of these */ + if (path_buffer[0] != '/') + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_FILENAME, + "Not a regular file or directory: %s", path_buffer); + return NULL; + } + + /* File descriptors to actually deleted files have " (deleted)" + appended to them. This also happens to some fake fd types + like shmem which are "/ (deleted)". All such + files are considered invalid. Unfortunately this also + matches files with filenames that actually end in " (deleted)", + but there is not much to do about this. */ + if (g_str_has_suffix (path_buffer, " (deleted)")) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_FILENAME, + "Cannot share deleted file: %s", path_buffer); + return NULL; + } + + /* remap from sandbox to host if needed */ + return xdp_app_info_remap_path (app_info, path_buffer); +} + +static char *documents_mountpoint = NULL; + +void +xdp_set_documents_mountpoint (const char *path) +{ + g_clear_pointer (&documents_mountpoint, g_free); + documents_mountpoint = g_strdup (path); +} + +/* alternate_document_path converts a file path */ +char * +xdp_get_alternate_document_path (const char *path, const char *app_id) +{ + int len; + + if (g_str_equal (app_id, "")) + return NULL; + + /* If we don't know where the document portal is mounted, then there + * is no alternate path */ + if (documents_mountpoint == NULL) + return NULL; + + /* If the path is not within the document portal, then there is no + * alternative path */ + len = strlen (documents_mountpoint); + if (!g_str_has_prefix (path, documents_mountpoint) || path[len] != '/') + return NULL; + + return g_strconcat (documents_mountpoint, "/by-app/", app_id, &path[len], NULL); +} + +static gboolean +check_same_file (const char *path, + struct stat *expected_st_buf, + GError **error) +{ + struct stat real_st_buf; + int saved_errno; + + if (stat (path, &real_st_buf) < 0) + { + saved_errno = errno; + g_set_error (error, G_IO_ERROR, g_io_error_from_errno (saved_errno), + "stat %s: %s", path, g_strerror (saved_errno)); + return FALSE; + } + + if (expected_st_buf->st_dev != real_st_buf.st_dev || + expected_st_buf->st_ino != real_st_buf.st_ino) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "\"%s\" identity (%ju,%ju) does not match expected (%ju,%ju)", + path, + (uintmax_t) expected_st_buf->st_dev, + (uintmax_t) expected_st_buf->st_ino, + (uintmax_t) real_st_buf.st_dev, + (uintmax_t) real_st_buf.st_ino); + return FALSE; + } + + return TRUE; +} + +char * +xdp_app_info_get_path_for_fd (XdpAppInfo *app_info, + int fd, + int require_st_mode, + struct stat *st_buf, + gboolean *writable_out, + GError **error) +{ + g_autofree char *proc_path = NULL; + int fd_flags; + struct stat st_buf_store; + gboolean writable = FALSE; + g_autofree char *path = NULL; + int saved_errno; + + if (st_buf == NULL) + st_buf = &st_buf_store; + + if (fd == -1) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + "Invalid file descriptor"); + return NULL; + } + + /* Must be able to get fd flags */ + fd_flags = fcntl (fd, F_GETFL); + if (fd_flags == -1) + { + saved_errno = errno; + g_set_error (error, G_IO_ERROR, g_io_error_from_errno (saved_errno), + "Cannot get file descriptor flags (fcntl F_GETFL: %s)", + g_strerror (saved_errno)); + return NULL; + } + + /* Must be able to fstat */ + if (fstat (fd, st_buf) < 0) + { + saved_errno = errno; + g_set_error (error, G_IO_ERROR, g_io_error_from_errno (saved_errno), + "Cannot get file information (fstat: %s)", + g_strerror (saved_errno)); + return NULL; + } + + /* Verify mode */ + if (require_st_mode != 0 && + (st_buf->st_mode & S_IFMT) != require_st_mode) + { + switch (require_st_mode) + { + case S_IFDIR: + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_DIRECTORY, + "File type 0o%o is not a directory", + (st_buf->st_mode & S_IFMT)); + return NULL; + + case S_IFREG: + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_REGULAR_FILE, + "File type 0o%o is not a regular file", + (st_buf->st_mode & S_IFMT)); + return NULL; + + default: + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + "File type 0o%o does not match expected 0o%o", + (st_buf->st_mode & S_IFMT), require_st_mode); + return NULL; + } + } + + proc_path = g_strdup_printf ("/proc/self/fd/%d", fd); + + /* Must be able to read valid path from /proc/self/fd */ + /* This is an absolute and (at least at open time) symlink-expanded path */ + path = verify_proc_self_fd (app_info, proc_path, error); + if (path == NULL) + return NULL; + + if ((fd_flags & O_PATH) == O_PATH) + { + int read_access_mode; + + /* Earlier versions of the portal supported only O_PATH fds, as + * these are safer to handle on the portal side. But we now + * prefer regular FDs because these ensure that the sandbox + * actually has full access to the file in its security context. + * + * However, we still support O_PATH fds when possible because + * existing code uses it. + * + * See issues #167 for details. + */ + + /* Must not be O_NOFOLLOW (because we want the target file) */ + if ((fd_flags & O_NOFOLLOW) == O_NOFOLLOW) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + "O_PATH fd was opened O_NOFOLLOW"); + return NULL; + } + + if (!xdp_app_info_supports_opath (app_info)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "App \"%s\" of type %d does not support O_PATH fd passing", + app_info->id, app_info->kind); + return NULL; + } + + read_access_mode = R_OK; + if (S_ISDIR (st_buf->st_mode)) + read_access_mode |= X_OK; + + /* Must be able to access the path via the sandbox supplied O_PATH fd, + which applies the sandbox side mount options (like readonly). */ + if (access (proc_path, read_access_mode) != 0) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED, + "\"%s\" not available for read access via \"%s\"", + path, proc_path); + return NULL; + } + + if (xdp_app_info_is_host (app_info) || access (proc_path, W_OK) == 0) + writable = TRUE; + } + else /* Regular file with no O_PATH */ + { + int accmode = fd_flags & O_ACCMODE; + + /* Note that this only gives valid results for writable for regular files, + as there is no way to get a writable fd for a directory. */ + + /* Don't allow WRONLY (or weird) open modes */ + if (accmode != O_RDONLY && + accmode != O_RDWR) + return NULL; + + if (xdp_app_info_is_host (app_info) || accmode == O_RDWR) + writable = TRUE; + } + + /* Verify that this is the same file as the app opened */ + if (!check_same_file (path, st_buf, error)) + { + /* If the path is provided by the document portal, the inode + number will not match, due to only a subtree being mounted in + the sandbox. So we check to see if the equivalent path + within that subtree matches our file descriptor. + + If the alternate path doesn't match either, then we treat it + as a failure. + */ + g_autofree char *alt_path = NULL; + alt_path = xdp_get_alternate_document_path (path, xdp_app_info_get_id (app_info)); + + if (alt_path == NULL) + return NULL; + + g_clear_error (error); + + if (!check_same_file (alt_path, st_buf, error)) + return NULL; + } + + if (writable_out) + *writable_out = writable; + + return g_steal_pointer (&path); +} + +static gboolean +is_valid_name_character (gint c, gboolean allow_dash) +{ + return + (c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + (c == '_') || (allow_dash && c == '-'); +} + +/* This is the same as flatpak apps, except we also allow + names to start with digits, and two-element names so that ids of the form + snap.$snapname is allowed for all snap names. */ +gboolean +xdp_is_valid_app_id (const char *string) +{ + guint len; + const gchar *s; + const gchar *end; + const gchar *last_dot; + int dot_count; + gboolean last_element; + + g_return_val_if_fail (string != NULL, FALSE); + + len = strlen (string); + if (G_UNLIKELY (len == 0)) + return FALSE; + + if (G_UNLIKELY (len > 255)) + return FALSE; + + end = string + len; + + last_dot = strrchr (string, '.'); + last_element = FALSE; + + s = string; + if (G_UNLIKELY (*s == '.')) + return FALSE; /* Name can't start with a period */ + + dot_count = 0; + while (s != end) + { + if (*s == '.') + { + if (s == last_dot) + last_element = TRUE; + s += 1; + if (G_UNLIKELY (s == end)) + return FALSE; + dot_count++; + } + + if (G_UNLIKELY (!is_valid_name_character (*s, last_element))) + return FALSE; + s += 1; + } + + if (G_UNLIKELY (dot_count < 1)) + return FALSE; + + return TRUE; +} + + +char * +xdp_quote_argv (const char *argv[]) +{ + GString *res = g_string_new (""); + int i; + + for (i = 0; argv[i] != NULL; i++) + { + if (i != 0) + g_string_append_c (res, ' '); + + if (needs_quoting (argv[i])) + { + g_autofree char *quoted = g_shell_quote (argv[i]); + g_string_append (res, quoted); + } + else + g_string_append (res, argv[i]); + } + + return g_string_free (res, FALSE); +} + +typedef struct +{ + GError *error; + GError *splice_error; + GMainLoop *loop; + int refs; +} SpawnData; + +static void +spawn_data_exit (SpawnData *data) +{ + data->refs--; + if (data->refs == 0) + g_main_loop_quit (data->loop); +} + +static void +spawn_output_spliced_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + SpawnData *data = user_data; + + g_output_stream_splice_finish (G_OUTPUT_STREAM (obj), result, &data->splice_error); + spawn_data_exit (data); +} + +static void +spawn_exit_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + SpawnData *data = user_data; + + g_subprocess_wait_check_finish (G_SUBPROCESS (obj), result, &data->error); + spawn_data_exit (data); +} + +gboolean +xdp_spawn (GFile *dir, + char **output, + GSubprocessFlags flags, + GError **error, + const gchar *argv0, + va_list ap) +{ + GPtrArray *args; + const gchar *arg; + gboolean res; + + args = g_ptr_array_new (); + g_ptr_array_add (args, (gchar *) argv0); + while ((arg = va_arg (ap, const gchar *))) + g_ptr_array_add (args, (gchar *) arg); + g_ptr_array_add (args, NULL); + + res = xdp_spawnv (dir, output, flags, error, (const gchar * const *) args->pdata); + + g_ptr_array_free (args, TRUE); + + return res; +} + +gboolean +xdp_spawnv (GFile *dir, + char **output, + GSubprocessFlags flags, + GError **error, + const gchar * const *argv) +{ + g_autoptr(GSubprocessLauncher) launcher = NULL; + g_autoptr(GSubprocess) subp = NULL; + GInputStream *in; + g_autoptr(GOutputStream) out = NULL; + g_autoptr(GMainLoop) loop = NULL; + SpawnData data = {0}; + g_autofree gchar *commandline = NULL; + + launcher = g_subprocess_launcher_new (0); + + if (output) + flags |= G_SUBPROCESS_FLAGS_STDOUT_PIPE; + + g_subprocess_launcher_set_flags (launcher, flags); + + if (dir) + { + g_autofree char *path = g_file_get_path (dir); + g_subprocess_launcher_set_cwd (launcher, path); + } + + commandline = xdp_quote_argv ((const char **)argv); + g_debug ("Running: %s", commandline); + + subp = g_subprocess_launcher_spawnv (launcher, argv, error); + + if (subp == NULL) + return FALSE; + + loop = g_main_loop_new (NULL, FALSE); + + data.loop = loop; + data.refs = 1; + + if (output) + { + data.refs++; + in = g_subprocess_get_stdout_pipe (subp); + out = g_memory_output_stream_new_resizable (); + g_output_stream_splice_async (out, + in, + G_OUTPUT_STREAM_SPLICE_NONE, + 0, + NULL, + spawn_output_spliced_cb, + &data); + } + + g_subprocess_wait_async (subp, NULL, spawn_exit_cb, &data); + + g_main_loop_run (loop); + + if (data.error) + { + g_propagate_error (error, data.error); + g_clear_error (&data.splice_error); + return FALSE; + } + + if (out) + { + if (data.splice_error) + { + g_propagate_error (error, data.splice_error); + return FALSE; + } + + /* Null terminate */ + g_output_stream_write (out, "\0", 1, NULL, NULL); + g_output_stream_close (out, NULL, NULL); + *output = g_memory_output_stream_steal_data (G_MEMORY_OUTPUT_STREAM (out)); + } + + return TRUE; +} + +char * +xdp_canonicalize_filename (const char *path) +{ + g_autoptr(GFile) file = g_file_new_for_path (path); + return g_file_get_path (file); +} + +gboolean +xdp_has_path_prefix (const char *str, + const char *prefix) +{ + while (TRUE) + { + /* Skip consecutive slashes to reach next path + element */ + while (*str == '/') + str++; + while (*prefix == '/') + prefix++; + + /* No more prefix path elements? Done! */ + if (*prefix == 0) + return TRUE; + + /* Compare path element */ + while (*prefix != 0 && *prefix != '/') + { + if (*str != *prefix) + return FALSE; + str++; + prefix++; + } + + /* Matched prefix path element, + must be entire str path element */ + if (*str != '/' && *str != 0) + return FALSE; + } +} + +/* pid mapping code */ +static int +parse_pid (const char *str, + pid_t *pid) +{ + char *end; + guint64 v; + pid_t p; + + errno = 0; + v = g_ascii_strtoull (str, &end, 0); + if (end == str) + return -ENOENT; + else if (errno != 0) + return -errno; + + p = (pid_t) v; + + if (p < 1 || (guint64) p != v) + return -ERANGE; + + if (pid) + *pid = p; + + return 0; +} + +static int +parse_status_field_pid (const char *val, + pid_t *pid) +{ + const char *t; + + t = strrchr (val, '\t'); + if (t == NULL) + return -ENOENT; + + return parse_pid (t, pid); +} + +static int +parse_status_field_uid (const char *val, + uid_t *uid) +{ + const char *t; + char *end; + guint64 v; + uid_t u; + + t = strrchr (val, '\t'); + if (t == NULL) + return -ENOENT; + + errno = 0; + v = g_ascii_strtoull (t, &end, 0); + if (end == val) + return -ENOENT; + else if (errno != 0) + return -errno; + + u = (uid_t) v; + + if ((guint64) u != v) + return -ERANGE; + + if (uid) + *uid = u; + + return 0; +} + +static int +parse_status_file (int pid_fd, + pid_t *pid_out, + uid_t *uid_out) +{ + g_autofree char *key = NULL; + g_autofree char *val = NULL; + gboolean have_pid = pid_out == NULL; + gboolean have_uid = uid_out == NULL; + FILE *f; + size_t keylen = 0; + size_t vallen = 0; + ssize_t n; + int fd; + int r = 0; + + g_return_val_if_fail (pid_fd > -1, FALSE); + + fd = openat (pid_fd, "status", O_RDONLY | O_CLOEXEC | O_NOCTTY); + if (fd == -1) + return -errno; + + f = fdopen (fd, "r"); + + if (f == NULL) + return -errno; + + fd = -1; /* fd is now owned by f */ + + do { + n = getdelim (&key, &keylen, ':', f); + if (n == -1) + { + r = -errno; + break; + } + + n = getdelim (&val, &vallen, '\n', f); + if (n == -1) + { + r = -errno; + break; + } + + g_strstrip (key); + g_strstrip (val); + + if (!strncmp (key, "NSpid", strlen ("NSpid"))) + { + r = parse_status_field_pid (val, pid_out); + have_pid = r > -1; + } + else if (!strncmp (key, "Uid", strlen ("Uid"))) + { + r = parse_status_field_uid (val, uid_out); + have_uid = r > -1; + } + + if (r < 0) + g_warning ("Failed to parse 'status::%s': %s", + key, g_strerror (-r)); + + } while (r == 0 && (!have_uid || !have_pid)); + + fclose (f); + + if (r != 0) + return r; + else if (!have_uid || !have_pid) + return -ENXIO; /* ENOENT for the fields */ + + return 0; +} + +static int +lookup_ns_from_pid_fd (int pid_fd, + ino_t *ns) +{ + struct stat st; + int r; + + g_return_val_if_fail (ns != NULL, -1); + + r = fstatat (pid_fd, "ns/pid", &st, 0); + if (r == -1) + return -errno; + + /* The inode number (together with the device ID) encode + * the identity of the pid namespace, see namespaces(7) + */ + *ns = st.st_ino; + + return 0; +} + +static int +open_pid_fd (int proc_fd, + pid_t pid, + GError **error) +{ + char buf[20] = {0, }; + int fd; + + snprintf (buf, sizeof(buf), "%u", (guint) pid); + + fd = openat (proc_fd, buf, O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC | O_NOCTTY); + + if (fd == -1) + g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errno), + "Could not to open '/proc/pid/%u': %s", (guint) pid, + g_strerror (errno)); + + return fd; +} + +static int +open_fdinfo_dir (GError **error) +{ + int fd; + + fd = open ("/proc/self/fdinfo", O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC | O_NOCTTY); + + if (fd < 0) + g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errno), + "Could not to open /proc/self/fdinfo: %s", + g_strerror (errno)); + + return fd; +} + +static inline gboolean +find_pid (pid_t *pids, + guint n_pids, + pid_t want, + guint *idx) +{ + for (guint i = 0; i < n_pids; i++) + { + if (pids[i] == want) + { + *idx = i; + return TRUE; + } + } + + return FALSE; +} + +static gboolean +map_pids (DIR *proc, + ino_t pidns, + pid_t *pids, + guint n_pids, + uid_t target_uid, + GError **error) +{ + pid_t *res = NULL; + struct dirent *de; + guint count = 0; + + res = g_alloca (sizeof (pid_t) * n_pids); + memset (res, 0, sizeof (pid_t) * n_pids); + + while ((de = readdir (proc)) != NULL) + { + xdp_autofd int pid_fd = -1; + pid_t outside = 0; + pid_t inside = 0; + uid_t uid = 0; + guint idx; + ino_t ns = 0; + int r; + + if (de->d_type != DT_DIR) + continue; + + pid_fd = openat (dirfd (proc), de->d_name, O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC | O_NOCTTY); + if (pid_fd == -1) + continue; + + r = lookup_ns_from_pid_fd (pid_fd, &ns); + if (r < 0) + continue; + + if (pidns != ns) + continue; + + r = parse_pid (de->d_name, &outside); + if (r < 0) + continue; + + r = parse_status_file (pid_fd, &inside, &uid); + if (r < 0) + continue; + + if (!find_pid (pids, n_pids, inside, &idx)) + continue; + + /* We got a match, let's make sure the real uids match as well */ + if (uid != target_uid) + { + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED, + "Matching pid doesn't belong to the target user"); + return FALSE; + } + + /* this handles the first occurrence, already identified by find_pid, + * as well as duplicate entries */ + for (guint i = idx; i < n_pids; i++) + { + if (pids[i] == inside) + { + res[idx] = outside; + count++; + } + } + } + + if (count != n_pids) + { + g_autoptr(GString) str = NULL; + + str = g_string_new ("Process ids could not be found: "); + + for (guint i = 0; i < n_pids; i++) + if (res[i] == 0) + g_string_append_printf (str, "%d, ", (guint32) pids[i]); + + g_string_truncate (str, str->len - 2); + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, str->str); + + return FALSE; + } + + memcpy (pids, res, sizeof (pid_t) * n_pids); + + return TRUE; +} + +static gboolean +pidfd_to_pid (int fdinfo, const int pidfd, pid_t *pid, GError **error) +{ + g_autofree char *name = NULL; + g_autofree char *key = NULL; + g_autofree char *val = NULL; + gboolean found = FALSE; + FILE *f = NULL; + size_t keylen = 0; + size_t vallen = 0; + ssize_t n; + int fd; + int r = 0; + + *pid = 0; + + name = g_strdup_printf ("%d", pidfd); + + fd = openat (fdinfo, name, O_RDONLY | O_CLOEXEC | O_NOCTTY); + + if (fd != -1) + f = fdopen (fd, "r"); + + if (f == NULL) + { + g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errno), + "Unable to open /proc/self/fdinfo/%d: %s", + fd, g_strerror (errno)); + return FALSE; + } + + do { + n = getdelim (&key, &keylen, ':', f); + if (n == -1) + { + r = errno; + break; + } + + n = getdelim (&val, &vallen, '\n', f); + if (n == -1) + { + r = errno; + break; + } + + g_strstrip (key); + + if (!strncmp (key, "Pid", 3)) + { + r = parse_status_field_pid (val, pid); + found = r > -1; + } + + } while (r == 0 && !found); + + fclose (f); + + if (r < 0) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Could not parse fdinfo::%s: %s", + key, g_strerror (-r)); + } + else if (!found) + { + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Could not parse fdinfo: Pid field missing"); + } + + return found; +} + +static JsonNode * +xdp_app_info_load_bwrap_info (XdpAppInfo *app_info, + GError **error) +{ + g_autoptr(JsonParser) parser = NULL; + g_autoptr(JsonNode) root = NULL; + g_autofree char *instance = NULL; + g_autofree char *data = NULL; + gsize len; + char *path; + + g_return_val_if_fail (app_info != NULL, 0); + + instance = xdp_app_info_get_instance (app_info); + + if (instance == NULL) + { + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Could not find instance-id in process's /.flatpak-info"); + return 0; + } + + path = g_build_filename (g_get_user_runtime_dir (), + ".flatpak", + instance, + "bwrapinfo.json", + NULL); + + if (!g_file_get_contents (path, &data, &len, error)) + return 0; + + parser = json_parser_new (); + if (!json_parser_load_from_data (parser, data, len, error)) + { + g_prefix_error (error, "Could not parse '%s': ", path); + return 0; + } + + root = json_node_ref (json_parser_get_root (parser)); + if (!root) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Could not parse '%s': empty file", path); + return 0; + } + + if (!JSON_NODE_HOLDS_OBJECT (root)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Could not parse '%s': invalid structure", path); + return 0; + } + + return g_steal_pointer (&root); +} + +static ino_t +xdp_app_info_get_pid_namespace (JsonNode *root, + GError **error) +{ + JsonNode *node; + JsonObject *cpo; + gint64 nsid; + + /* xdp_app_info_load_bwrap_info assures root is of type object */ + cpo = json_node_get_object (root); + node = json_object_get_member (cpo, "pid-namespace"); + + if (node == NULL || !JSON_NODE_HOLDS_VALUE (node)) + { + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "pid-namespace missing"); + return 0; + } + + nsid = json_node_get_int (node); + return (ino_t) nsid; +} + +static pid_t +xdp_app_info_get_child_pid (JsonNode *root, + GError **error) +{ + JsonObject *cpo; + pid_t pid; + + cpo = json_node_get_object (root); + + pid = json_object_get_int_member (cpo, "child-pid"); + if (pid == 0) + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "child-pid missing"); + + return pid; +} + +static gboolean +xdp_app_info_ensure_pidns (XdpAppInfo *app_info, + DIR *proc, + GError **error) +{ + g_autoptr(JsonNode) root = NULL; + g_autoptr(GMutexLocker) guard = NULL; + xdp_autofd int fd = -1; + pid_t pid; + ino_t ns; + int r; + + g_assert (app_info->kind == XDP_APP_INFO_KIND_FLATPAK); + + guard = g_mutex_locker_new (&(app_info->u.flatpak.pidns_lock)); + + if (app_info->u.flatpak.pidns_id != 0) + return TRUE; + + root = xdp_app_info_load_bwrap_info (app_info, error); + if (root == NULL) + return FALSE; + + /* newer versions of bubblewrap contain the namespace + * information directly, so we don' thave to go via the + * child-pid; if this fails, we fallback to the old way */ + ns = xdp_app_info_get_pid_namespace (root, NULL); + if (ns != 0) + { + g_debug ("Using pid namespace info from bwrap info"); + app_info->u.flatpak.pidns_id = ns; + return TRUE; + } + + pid = xdp_app_info_get_child_pid (root, error); + if (pid == 0) + return FALSE; + + fd = open_pid_fd (dirfd (proc), pid, error); + if (fd == -1) + return FALSE; + + r = lookup_ns_from_pid_fd (fd, &ns); + if (r < 0) + { + int code = g_io_error_from_errno (-r); + g_set_error (error, G_IO_ERROR, code, + "Could not query /proc/%u/ns/pid: %s", + (guint) pid, g_strerror (-r)); + return FALSE; + } + + app_info->u.flatpak.pidns_id = ns; + + return TRUE; +} + +/* This is the trunk for xdp_app_info_map_pids()/xdp_app_info_map_tids() */ +static gboolean +app_info_map_pids (XdpAppInfo *app_info, + const char *proc_dir, + pid_t *pids, + guint n_pids, + GError **error) +{ + gboolean ok; + DIR *proc; + uid_t uid; + ino_t ns; + + g_return_val_if_fail (app_info != NULL, FALSE); + g_return_val_if_fail (pids != NULL, FALSE); + + if (app_info->kind != XDP_APP_INFO_KIND_FLATPAK) + { + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Mapping pids is not supported."); + return FALSE; + } + + proc = opendir (proc_dir); + if (proc == NULL) + { + g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errno), + "Could not open '%s': %s", proc_dir, g_strerror (errno)); + return FALSE; + } + + /* Make sure we know the pid namespace the app is running in */ + ok = xdp_app_info_ensure_pidns (app_info, proc, error); + if (!ok) + { + g_prefix_error (error, "Could not determine pid namespace: "); + goto out; + } + + /* we also make sure the real user id matches + * to the process owner we are trying to resolve + */ + uid = getuid (); + + ns = app_info->u.flatpak.pidns_id; + ok = map_pids (proc, ns, pids, n_pids, uid, error); + + out: + closedir (proc); + return ok; +} + + +gboolean +xdp_app_info_map_tids (XdpAppInfo *app_info, + pid_t owner_pid, + pid_t *tids, + guint n_tids, + GError **error) +{ + g_autofree char *proc_dir = g_strdup_printf ("/proc/%u/task", (guint) owner_pid); + return app_info_map_pids (app_info, proc_dir, tids, n_tids, error); +} + +gboolean +xdp_app_info_map_pids (XdpAppInfo *app_info, + pid_t *pids, + guint n_pids, + GError **error) +{ + return app_info_map_pids (app_info, "/proc", pids, n_pids, error); +} + +gboolean +xdp_app_info_pidfds_to_pids (XdpAppInfo *app_info, + const int *fds, + pid_t *pids, + gint count, + GError **error) +{ + gboolean ok = TRUE; + int fdinfo = -1; + + g_return_val_if_fail (app_info != NULL, FALSE); + g_return_val_if_fail (fds != NULL, FALSE); + g_return_val_if_fail (pids != NULL, FALSE); + + fdinfo = open_fdinfo_dir (error); + if (fdinfo == -1) + return FALSE; + + for (gint i = 0; i < count && ok; i++) + ok = pidfd_to_pid (fdinfo, fds[i], &pids[i], error); + + (void) close (fdinfo); + + return ok; +} + +void +cleanup_temp_file (void *p) +{ + void **pp = (void **)p; + + if (*pp) + remove (*pp); + g_free (*pp); +} + +#define ICON_VALIDATOR_GROUP "Icon Validator" + +gboolean +xdp_validate_serialized_icon (GVariant *v, + gboolean bytes_only, + char **out_format, + char **out_size) +{ + g_autoptr(GIcon) icon = NULL; + GBytes *bytes; + __attribute__((cleanup(cleanup_temp_file))) char *name = NULL; + xdp_autofd int fd = -1; + g_autoptr(GOutputStream) stream = NULL; + int status; + g_autofree char *format = NULL; + g_autofree char *stdoutlog = NULL; + g_autofree char *stderrlog = NULL; + g_autoptr(GError) error = NULL; + const char *icon_validator = LIBEXECDIR "/xdg-desktop-portal-validate-icon"; + const char *args[6]; + /* same allowed formats as Flatpak */ + const char *allowed_icon_formats[] = { "png", "jpeg", "svg", NULL }; + int size; + const char *MAX_ICON_SIZE = "512"; + gconstpointer bytes_data; + gsize bytes_len; + g_autoptr(GKeyFile) key_file = NULL; + + icon = g_icon_deserialize (v); + if (!icon) + { + g_warning ("Icon deserialization failed"); + return FALSE; + } + + if (!bytes_only && G_IS_THEMED_ICON (icon)) + { + g_autofree char *a = g_strjoinv (" ", (char **)g_themed_icon_get_names (G_THEMED_ICON (icon))); + g_debug ("Icon validation: themed icon (%s) is ok", a); + return TRUE; + } + + if (!G_IS_BYTES_ICON (icon)) + { + g_warning ("Unexpected icon type: %s", G_OBJECT_TYPE_NAME (icon)); + return FALSE; + } + + if (!g_file_test (icon_validator, G_FILE_TEST_EXISTS)) + { + g_warning ("Icon validation: %s not found, rejecting icon by default.", icon_validator); + return FALSE; + } + + bytes = g_bytes_icon_get_bytes (G_BYTES_ICON (icon)); + fd = g_file_open_tmp ("iconXXXXXX", &name, &error); + if (fd == -1) + { + g_warning ("Icon validation: %s", error->message); + return FALSE; + } + + stream = g_unix_output_stream_new (fd, FALSE); + + /* Use write_all() instead of write_bytes() so we don't have to worry about + * partial writes (https://gitlab.gnome.org/GNOME/glib/-/issues/570). + */ + bytes_data = g_bytes_get_data (bytes, &bytes_len); + if (!g_output_stream_write_all (stream, bytes_data, bytes_len, NULL, NULL, &error)) + { + g_warning ("Icon validation: %s", error->message); + return FALSE; + } + + if (!g_output_stream_close (stream, NULL, &error)) + { + g_warning ("Icon validation: %s", error->message); + return FALSE; + } + + args[0] = icon_validator; + args[1] = "--sandbox"; + args[2] = MAX_ICON_SIZE; + args[3] = MAX_ICON_SIZE; + args[4] = name; + args[5] = NULL; + + if (!g_spawn_sync (NULL, (char **)args, NULL, 0, NULL, NULL, &stdoutlog, &stderrlog, &status, &error)) + { + g_warning ("Icon validation: %s", error->message); + g_warning ("stderr:\n%s\n", stderrlog); + return FALSE; + } + + if (!g_spawn_check_exit_status (status, &error)) + { + g_warning ("Icon validation: %s", error->message); + return FALSE; + } + + key_file = g_key_file_new (); + if (!g_key_file_load_from_data (key_file, stdoutlog, -1, G_KEY_FILE_NONE, &error)) + { + g_warning ("Icon validation: %s", error->message); + return FALSE; + } + if (!(format = g_key_file_get_string (key_file, ICON_VALIDATOR_GROUP, "format", &error)) || + !g_strv_contains (allowed_icon_formats, format)) + { + g_warning ("Icon validation: %s", error ? error->message : "not allowed format"); + return FALSE; + } + if (!(size = g_key_file_get_integer (key_file, ICON_VALIDATOR_GROUP, "width", &error))) + { + g_warning ("Icon validation: %s", error->message); + return FALSE; + } + + if (out_format) + *out_format = g_steal_pointer (&format); + if (out_size) + *out_size = g_strdup_printf ("%d", size); + + return TRUE; +} + +gboolean +xdp_variant_contains_key (GVariant *dictionary, + const char *key) +{ + GVariantIter iter; + + g_variant_iter_init (&iter, dictionary); + while (TRUE) + { + g_autoptr(GVariant) entry = NULL; + g_autoptr(GVariant) entry_key = NULL; + + entry = g_variant_iter_next_value (&iter); + if (!entry) + break; + + entry_key = g_variant_get_child_value (entry, 0); + if (g_strcmp0 (g_variant_get_string (entry_key, NULL), key) == 0) + return TRUE; + } + + return FALSE; +} diff --git a/src/xdp-utils.h b/src/xdp-utils.h new file mode 100644 index 0000000..65e9410 --- /dev/null +++ b/src/xdp-utils.h @@ -0,0 +1,208 @@ +/* + * Copyright © 2014, 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Alexander Larsson + * Matthias Clasen + */ + +#pragma once + +#include +#include +#include +#include + +#include +#include + +#include "glib-backports.h" + +#define DESKTOP_PORTAL_OBJECT_PATH "/org/freedesktop/portal/desktop" + +#define FLATPAK_METADATA_GROUP_APPLICATION "Application" +#define FLATPAK_METADATA_KEY_NAME "name" +#define FLATPAK_METADATA_GROUP_INSTANCE "Instance" +#define FLATPAK_METADATA_KEY_APP_PATH "app-path" +#define FLATPAK_METADATA_KEY_ORIGINAL_APP_PATH "original-app-path" +#define FLATPAK_METADATA_KEY_RUNTIME_PATH "runtime-path" +#define FLATPAK_METADATA_KEY_INSTANCE_ID "instance-id" + +#define SNAP_METADATA_GROUP_INFO "Snap Info" +#define SNAP_METADATA_KEY_INSTANCE_NAME "InstanceName" +#define SNAP_METADATA_KEY_DESKTOP_FILE "DesktopFile" +#define SNAP_METADATA_KEY_NETWORK "HasNetworkStatus" + +typedef enum +{ + XDP_APP_INFO_KIND_HOST = 0, + XDP_APP_INFO_KIND_FLATPAK = 1, + XDP_APP_INFO_KIND_SNAP = 2, +} XdpAppInfoKind; + +gint xdp_mkstempat (int dir_fd, + gchar *tmpl, + int flags, + int mode); + +gboolean xdp_is_valid_app_id (const char *string); + +gboolean xdp_validate_serialized_icon (GVariant *v, + gboolean bytes_only, + char **out_format, + char **out_size); + +typedef void (*XdpPeerDiedCallback) (const char *name); + +typedef struct _XdpAppInfo XdpAppInfo; + +typedef int XdpFd; +G_DEFINE_AUTO_CLEANUP_FREE_FUNC(XdpFd, close, -1) + +XdpAppInfo *xdp_app_info_ref (XdpAppInfo *app_info); +void xdp_app_info_unref (XdpAppInfo *app_info); +const char *xdp_app_info_get_id (XdpAppInfo *app_info); +char * xdp_app_info_get_instance (XdpAppInfo *app_info); +gboolean xdp_app_info_is_host (XdpAppInfo *app_info); +XdpAppInfoKind xdp_app_info_get_kind (XdpAppInfo *app_info); +gboolean xdp_app_info_supports_opath (XdpAppInfo *app_info); +char * xdp_app_info_remap_path (XdpAppInfo *app_info, + const char *path); +gboolean xdp_app_info_map_pids (XdpAppInfo *app_info, + pid_t *pids, + guint n_pids, + GError **error); +gboolean xdp_app_info_map_tids (XdpAppInfo *app_info, + pid_t owner_pid, + pid_t *tids, + guint n_tids, + GError **error); +gboolean xdp_app_info_pidfds_to_pids (XdpAppInfo *app_info, + const int *fds, + pid_t *pids, + gint count, + GError **error); +char * xdp_app_info_get_path_for_fd (XdpAppInfo *app_info, + int fd, + int require_st_mode, + struct stat *st_buf, + gboolean *writable_out, + GError **error); +gboolean xdp_app_info_has_network (XdpAppInfo *app_info); +XdpAppInfo *xdp_get_app_info_from_pid (pid_t pid, + GError **error); +GAppInfo * xdp_app_info_load_app_info (XdpAppInfo *app_info); +char ** xdp_app_info_rewrite_commandline (XdpAppInfo *app_info, + const char *const *commandline, + gboolean quote_escape); +char *xdp_app_info_get_tryexec_path (XdpAppInfo *app_info); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(XdpAppInfo, xdp_app_info_unref) + +void xdp_set_documents_mountpoint (const char *path); +char *xdp_get_alternate_document_path (const char *path, const char *app_id); + +XdpAppInfo *xdp_invocation_lookup_app_info_sync (GDBusMethodInvocation *invocation, + GCancellable *cancellable, + GError **error); +void xdp_connection_track_name_owners (GDBusConnection *connection, + XdpPeerDiedCallback peer_died_cb); + +gboolean xdp_variant_contains_key (GVariant *dictionary, + const char *key); + +typedef struct { + const char *key; + const GVariantType *type; + gboolean (* validate) (const char *key, GVariant *value, GVariant *options, GError **error); +} XdpOptionKey; + +gboolean xdp_filter_options (GVariant *options_in, + GVariantBuilder *options_out, + XdpOptionKey *supported_options, + int n_supported_options, + GError **error); + +typedef enum { + XDG_DESKTOP_PORTAL_ERROR_FAILED = 0, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + XDG_DESKTOP_PORTAL_ERROR_NOT_FOUND, + XDG_DESKTOP_PORTAL_ERROR_EXISTS, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + XDG_DESKTOP_PORTAL_ERROR_CANCELLED, + XDG_DESKTOP_PORTAL_ERROR_WINDOW_DESTROYED +} XdgDesktopPortalErrorEnum; + +#define XDG_DESKTOP_PORTAL_ERROR xdg_desktop_portal_error_quark () + +GQuark xdg_desktop_portal_error_quark (void); + +static inline int +xdp_steal_fd (int *fdp) +{ + int fd = *fdp; + *fdp = -1; + return fd; +} + +static inline void +xdp_close_fd (int *fdp) +{ + int errsv; + + g_assert (fdp); + + int fd = xdp_steal_fd (fdp); + if (fd >= 0) + { + errsv = errno; + if (close (fd) < 0) + g_assert (errno != EBADF); + errno = errsv; + } +} + +#define xdp_autofd __attribute__((cleanup(xdp_close_fd))) + +#define XDP_AUTOLOCK(name) \ + g_autoptr(GMutexLocker) G_PASTE (name ## locker, __LINE__) = \ + g_mutex_locker_new (&G_LOCK_NAME (name)); \ + (void) G_PASTE (name ## locker, __LINE__); + + +char * xdp_quote_argv (const char *argv[]); +gboolean xdp_spawn (GFile *dir, + char **output, + GSubprocessFlags flags, + GError **error, + const gchar *argv0, + va_list ap); +gboolean xdp_spawnv (GFile *dir, + char **output, + GSubprocessFlags flags, + GError **error, + const gchar * const *argv); + +char * xdp_canonicalize_filename (const char *path); +gboolean xdp_has_path_prefix (const char *str, + const char *prefix); + +/* exposed for the benefit of tests */ +int _xdp_parse_cgroup_file (FILE *f, + gboolean *is_snap); +#ifdef HAVE_LIBSYSTEMD +char *_xdp_parse_app_id_from_unit_name (const char *unit); +#endif diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..93b2eb2 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,512 @@ +#!/usr/bin/env python3 +# +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is formatted with Python Black +# + +# Shared setup for portal tests. To test a portal, subclass TestPortal with +# your portal's name (e.g. TestEmail). This will auto-fill your portal +# name into some of the functions. +# +# Make sure the portal is listed in tests/portals/test.portal and you have a +# dbusmock template for the impl.portal of your portal in tests/templates. See +# the dbusmock documentation for details on those templates. +# +# Environment variables: +# G_TEST_BUILDDIR: override the path to the tests/ build +# directory (default: $PWD) +# LIBEXECDIR: run xdg-desktop-portal from that dir +# XDP_DBUS_MONITOR: if set, starts dbus_monitor on the custom bus, useful +# for debugging + +from dbus.mainloop.glib import DBusGMainLoop +from gi.repository import GLib +from itertools import count +from typing import Any, Dict, Optional, NamedTuple +from pathlib import Path + +import dbus +import dbus.proxies +import dbusmock +import fcntl +import logging +import os +import subprocess +import time + +DBusGMainLoop(set_as_default=True) + +# Anything that takes longer than 5s needs to fail +MAX_TIMEOUT = 5000 + +_counter = count() + +ASV = Dict[str, Any] + +logger = logging.getLogger("tests") + + +class Response(NamedTuple): + """ + Response as returned by a completed :class:`Request` + """ + + response: int + results: ASV + + +class ResponseTimeout(Exception): + """ + Exception raised by :meth:`Request.call` if the Request did not receive a + Response in time. + """ + + pass + + +class Closable: + """ + Parent class for both Session and Request. Both of these have a Close() + method. + """ + + def __init__(self, bus: dbus.Bus, objpath: str): + self.objpath = objpath + # GLib makes assertions in callbacks impossible, so we wrap all + # callbacks into a try: except and store the error on the request to + # be raised later when we're back in the main context + self.error = None + + self._mainloop: Optional[GLib.MainLoop] = None + self._impl_closed = False + self._bus = bus + + self._closable = type(self).__name__ + assert self._closable in ("Request", "Session") + proxy = bus.get_object("org.freedesktop.portal.Desktop", objpath) + self._closable_interface = dbus.Interface( + proxy, f"org.freedesktop.portal.{self._closable}" + ) + + @property + def bus(self) -> dbus.Bus: + return self._bus + + @property + def closed(self) -> bool: + """ + True if the impl.portal was closed + """ + return self._impl_closed + + def close(self) -> None: + signal_match = None + + def cb_impl_closed_by_portal(handle) -> None: + if handle == self.objpath: + logger.debug(f"Impl{self._closable} {self.objpath} was closed") + signal_match.remove() # type: ignore + self._impl_closed = True + if self.closed and self._mainloop: + self._mainloop.quit() + + # See :class:`ImplRequest`, this signal is a side-channel for the + # impl.portal template to notify us when the impl.Request was really + # closed by the portal. + signal_match = self._bus.add_signal_receiver( + cb_impl_closed_by_portal, + f"{self._closable}Closed", + dbus_interface="org.freedesktop.impl.portal.Test", + ) + + logger.debug(f"Closing {self._closable} {self.objpath}") + self._closable_interface.Close() + + def schedule_close(self, timeout_ms=300): + """ + Schedule an automatic Close() on the given timeout in milliseconds. + """ + assert 0 < timeout_ms < MAX_TIMEOUT + GLib.timeout_add(timeout_ms, self.close) + + +class Request(Closable): + """ + Helper class for executing methods that use Requests. This calls takes + care of subscribing to the signals and invokes the method on the + interface with the expected behaviors. A typical invocation is: + + >>> response = Request(connection, interface).call("Foo", bar="bar") + >>> assert response.response == 0 + + Requests can only be used once, to call a second method you must + instantiate a new Request object. + """ + + def __init__(self, bus: dbus.Bus, interface: dbus.Interface): + def sanitize(name): + return name.lstrip(":").replace(".", "_") + + sender_token = sanitize(bus.get_unique_name()) + self._handle_token = f"request{next(_counter)}" + self.handle = f"/org/freedesktop/portal/desktop/request/{sender_token}/{self._handle_token}" + # The Closable + super().__init__(bus, self.handle) + + self.interface = interface + self.response: Optional[Response] = None + self.used = False + # GLib makes assertions in callbacks impossible, so we wrap all + # callbacks into a try: except and store the error on the request to + # be raised later when we're back in the main context + self.error: Optional[Exception] = None + + proxy = bus.get_object("org.freedesktop.portal.Desktop", self.handle) + self.mock_interface = dbus.Interface(proxy, dbusmock.MOCK_IFACE) + self._proxy = bus.get_object("org.freedesktop.portal.Desktop", self.handle) + + def cb_response(response: int, results: ASV) -> None: + try: + logger.debug(f"Response received on {self.handle}") + assert self.response is None + self.response = Response(response, results) + if self._mainloop: + self._mainloop.quit() + except Exception as e: + self.error = e + + self.request_interface = dbus.Interface(proxy, "org.freedesktop.portal.Request") + self.request_interface.connect_to_signal("Response", cb_response) + + @property + def handle_token(self) -> dbus.String: + """ + Returns the dbus-ready handle_token, ready to be put into the options + """ + return dbus.String(self._handle_token, variant_level=1) + + def call(self, methodname: str, **kwargs) -> Optional[Response]: + """ + Semi-synchronously call method ``methodname`` on the interface given + in the Request's constructor. The kwargs must be specified in the + order the DBus method takes them but the handle_token is automatically + filled in. + + >>> response = Request(connection, interface).call("Foo", bar="bar") + >>> if response.response != 0: + ... print("some error occured") + + The DBus call itself is asynchronous (required for signals to work) + but this method does not return until the Response is received, the + Request is closed or an error occurs. If the Request is closed, the + Response is None. + + If the "reply_handler" and "error_handler" keywords are present, those + callbacks are called just like they would be as dbus.service.ProxyObject. + """ + assert not self.used + self.used = True + + # Make sure options exists and has the handle_token set + try: + options = kwargs["options"] + except KeyError: + options = dbus.Dictionary({}, signature="sv") + + if "handle_token" not in options: + options["handle_token"] = self.handle_token + + # Anything that takes longer than 5s needs to fail + self._mainloop = GLib.MainLoop() + GLib.timeout_add(MAX_TIMEOUT, self._mainloop.quit) + + method = getattr(self.interface, methodname) + assert method + + reply_handler = kwargs.pop("reply_handler", None) + error_handler = kwargs.pop("error_handler", None) + + # Handle the normal method reply which returns is the Request object + # path. We don't exit the mainloop here, we're waiting for either the + # Response signal on the Request itself or the Close() handling + def reply_cb(handle): + try: + logger.debug(f"Reply to {methodname} with {self.handle}") + assert handle == self.handle + + if reply_handler: + reply_handler(handle) + except Exception as e: + self.error = e + + # Handle any exceptions during the actual method call (not the Request + # handling itself). Can exit the mainloop if that happens + def error_cb(error): + try: + logger.debug(f"Error after {methodname} with {error}") + if error_handler: + error_handler(error) + self.error = error + except Exception as e: + self.error = e + finally: + if self._mainloop: + self._mainloop.quit() + + # Method is invoked async, otherwise we can't mix and match signals + # and other calls. It's still sync as seen by the caller in that we + # have a mainloop that waits for us to finish though. + method( + *list(kwargs.values()), + reply_handler=reply_cb, + error_handler=error_cb, + ) + + self._mainloop.run() + + if self.error: + raise self.error + elif not self.closed and self.response is None: + raise ResponseTimeout(f"Timed out waiting for response from {methodname}") + + return self.response + + +class Session(Closable): + """ + Helper class for a Session created by a portal. This class takes care of + subscribing to the `Closed` signals. A typical invocation is: + + >>> response = Request(connection, interface).call("CreateSession") + >>> session = Session.from_response(response) + # Now run the main loop and do other stuff + # Check if the session was closed + >>> if session.closed: + ... pass + # or close the session explicitly + >>> session.close() # to close the session or + """ + + def __init__(self, bus: dbus.Bus, handle: str): + assert handle + super().__init__(bus, handle) + + self.handle = handle + self.details = None + # GLib makes assertions in callbacks impossible, so we wrap all + # callbacks into a try: except and store the error on the request to + # be raised later when we're back in the main context + self.error = None + self._closed_sig_received = False + + def cb_closed(details: ASV) -> None: + try: + logger.debug(f"Session.Closed received on {self.handle}") + assert not self._closed_sig_received + self._closed_sig_received = True + self.details = details + if self._mainloop: + self._mainloop.quit() + except Exception as e: + self.error = e + + proxy = bus.get_object("org.freedesktop.portal.Desktop", handle) + self.session_interface = dbus.Interface(proxy, "org.freedesktop.portal.Session") + self.session_interface.connect_to_signal("Closed", cb_closed) + + @property + def closed(self): + """ + Returns True if the session was closed by the backend + """ + return self._closed_sig_received or super().closed + + @classmethod + def from_response(cls, bus: dbus.Bus, response: Response) -> "Session": + return cls(bus, response.results["session_handle"]) + + +class PortalMock: + """ + Parent class for portal tests. + """ + + def __init__(self, session_bus, portal_name: str): + self.bus = session_bus + self.portal_name = portal_name + self.p_mock = None + self.xdp = None + self.portal_interfaces: Dict[str, dbus.Interface] = {} + self.dbus_monitor = None + + @property + def interface_name(self) -> str: + return f"org.freedesktop.portal.{self.portal_name}" + + @property + def dbus_con(self): + return self.bus.dbus_con + + def start_impl_portal(self, params=None, portal=None): + """ + Start the impl.portal for the given portal name. If missing, + the portal name is derived from the class name of the test, e.g. + ``TestFoo`` will start ``org.freedesktop.impl.portal.Foo``. + """ + portal = portal or self.portal_name + self.p_mock, self.obj_portal = self.bus.spawn_server_template( + template=f"tests/templates/{portal.lower()}.py", + parameters=params, + stdout=subprocess.PIPE, + ) + flags = fcntl.fcntl(self.p_mock.stdout, fcntl.F_GETFL) + fcntl.fcntl(self.p_mock.stdout, fcntl.F_SETFL, flags | os.O_NONBLOCK) + self.mock_interface = dbus.Interface(self.obj_portal, dbusmock.MOCK_IFACE) + + self.start_dbus_monitor() + + def add_template(self, portal, params: Dict[str, Any] = {}): + """ + Add an additional template to the portal object + """ + + self.obj_portal.AddTemplate( + f"tests/templates/{portal.lower()}.py", + dbus.Dictionary(params, signature="sv"), + dbus_interface=dbusmock.MOCK_IFACE, + ) + + def start_xdp(self): + """ + Start the xdg-desktop-portal process + """ + + # This roughly resembles test-portals.c and glib's test behavior + # but preferences in-tree testing by running pytest in meson's + # project_build_root + libexecdir = os.getenv("LIBEXECDIR") + if libexecdir: + xdp_path = Path(libexecdir) / "xdg-desktop-portal" + else: + xdp_path = ( + Path(os.getenv("G_TEST_BUILDDIR") or "tests") + / ".." + / "src" + / "xdg-desktop-portal" + ) + + if not xdp_path.exists(): + raise FileNotFoundError( + f"{xdp_path} does not exist, try running from meson build dir or setting G_TEST_BUILDDIR" + ) + + portal_dir = Path(os.getenv("G_TEST_BUILDDIR") or "tests") / "portals" / "test" + if not portal_dir.exists(): + raise FileNotFoundError( + f"{portal_dir} does not exist, try running from meson build dir or setting G_TEST_SRCDIR" + ) + + argv = [xdp_path] + env = os.environ.copy() + env["G_DEBUG"] = "fatal-criticals" + env["XDG_DESKTOP_PORTAL_DIR"] = portal_dir + env["XDG_CURRENT_DESKTOP"] = "test" + + xdp = subprocess.Popen(argv, env=env) + + for _ in range(50): + if self.bus.dbus_con.name_has_owner("org.freedesktop.portal.Desktop"): + break + time.sleep(0.1) + else: + assert ( + False + ), "Timeout while waiting for xdg-desktop-portal to claim the bus" + + self.xdp = xdp + + def start_dbus_monitor(self): + if not os.getenv("XDP_DBUS_MONITOR"): + return + + argv = ["dbus-monitor", "--session"] + self.dbus_monitor = subprocess.Popen(argv) + + def tearDown(self): + if self.dbus_monitor: + self.dbus_monitor.terminate() + self.dbus_monitor.wait() + + if self.xdp: + self.xdp.terminate() + self.xdp.wait() + + if self.p_mock: + if self.p_mock.stdout: + out = (self.p_mock.stdout.read() or b"").decode("utf-8") + if out: + print(out) + self.p_mock.stdout.close() + self.p_mock.terminate() + self.p_mock.wait() + + def get_xdp_dbus_object(self) -> dbus.proxies.ProxyObject: + """ + Return the object that is the org.freedesktop.portal.Desktop proxy + """ + try: + return self._xdp_dbus_object + except AttributeError: + obj = self.bus.dbus_con.get_object( + "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop" + ) + # Useful for debugging: + # print(obj.Introspect(dbus_interface="org.freedesktop.DBus.Introspectable")) + assert obj + self._xdp_dbus_object: dbus.proxies.ProxyObject = obj + return self._xdp_dbus_object + + def get_dbus_interface(self, name=None) -> dbus.Interface: + """ + Return the interface with the given name. + + >>> my_portal_intf = self.get_dbus_interface() + >>> rd_portal_intf = self.get_dbus_interface("RemoteDesktop") + >>> dbus_intf = self.get_dbus_interface("org.freedesktop.DBus.Introspectable") + + For portals, it's enough to specify the portal name (e.g. "InputCapture"). + If no name is provided, guess from the test class name. + """ + name = name or self.interface_name + if "." not in name: + name = f"org.freedesktop.portal.{name}" + + try: + intf = getattr(self, "portal_interfaces", {})[name] + except KeyError: + intf = dbus.Interface(self.get_xdp_dbus_object(), name) + assert intf + self.portal_interfaces[name] = intf + return intf + + def create_request(self, intf_name: Optional[str] = None) -> Request: + intf = self.get_dbus_interface(intf_name) + return Request(self.dbus_con, intf) + + def check_version(self, expected_version): + """ + Helper function to check for a portal's version. Use as: + + >>> class TestFoo(PortalMock): + ... def test_version(self): + ... self.check_version(2) + >>> + """ + properties_intf = self.get_dbus_interface("org.freedesktop.DBus.Properties") + try: + portal_version = properties_intf.Get(self.interface_name, "version") + assert int(portal_version) == expected_version + except dbus.exceptions.DBusException as e: + logger.critical(e) + assert e is None, str(e) diff --git a/tests/account.c b/tests/account.c new file mode 100644 index 0000000..3208a84 --- /dev/null +++ b/tests/account.c @@ -0,0 +1,330 @@ +#include + +#include "account.h" + +#include + +extern char outdir[]; + +/* We use g_main_context_wakeup() and a boolean variable + * to make the test cases wait for async calls to return + * without a maze of callbacks. + * + * The tests communicate with the backend via a keyfile + * in a shared location. + */ +static int got_info = 0; + +static void +account_cb (GObject *obj, + GAsyncResult *result, + gpointer data) +{ + XdpPortal *portal = XDP_PORTAL (obj); + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) ret = NULL; + GKeyFile *keyfile = data; + gboolean res; + const char *s; + char *t; + int response; + + response = g_key_file_get_integer (keyfile, "result", "response", NULL); + + ret = xdp_portal_get_user_information_finish (portal, result, &error); + if (response == 0) + { + g_assert_no_error (error); + + t = g_key_file_get_string (keyfile, "account", "id", NULL); + res = g_variant_lookup (ret, "id", "&s", &s); + g_assert (res == (t != NULL)); + if (t) g_assert_cmpstr (s, ==, t); + + t = g_key_file_get_string (keyfile, "account", "name", NULL); + res = g_variant_lookup (ret, "name", "&s", &s); + g_assert (res == (t != NULL)); + if (t) g_assert_cmpstr (s, ==, t); + + t = g_key_file_get_string (keyfile, "account", "image", NULL); + res = g_variant_lookup (ret, "image", "&s", &s); + g_assert (res == (t != NULL)); + if (t) g_assert_cmpstr (s, ==, t); + } + else if (response == 1) + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED); + else if (response == 2) + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_FAILED); + else + g_assert_not_reached (); + + got_info++; + + g_main_context_wakeup (NULL); +} + +static void +account_cb_fail (GObject *obj, + GAsyncResult *result, + gpointer data) +{ + XdpPortal *portal = XDP_PORTAL (obj); + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) ret = NULL; + + ret = xdp_portal_get_user_information_finish (portal, result, &error); + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_FAILED); + + got_info++; + g_main_context_wakeup (NULL); +} + +/* some basic tests using libportal, and test that communication + * with the backend via keyfile works + */ +void +test_account_basic (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + keyfile = g_key_file_new (); + + g_key_file_set_string (keyfile, "account", "id", "test"); + g_key_file_set_string (keyfile, "account", "name", "Donald Duck"); + g_key_file_set_string (keyfile, "account", "image", ""); + + g_key_file_set_string (keyfile, "backend", "reason", "test"); + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "account", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_get_user_information (portal, NULL, "test", 0, NULL, account_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +/* check that the reason argument makes it to the backend + */ +void +test_account_reason (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + const char *long_reason; + + keyfile = g_key_file_new (); + + g_key_file_set_string (keyfile, "account", "id", "test"); + g_key_file_set_string (keyfile, "account", "name", "Donald Duck"); + + g_key_file_set_string (keyfile, "backend", "reason", "xx"); + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "account", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_get_user_information (portal, NULL, "xx", 0, NULL, account_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); + + got_info = 0; + xdp_portal_get_user_information (portal, NULL, "yy", 0, NULL, account_cb_fail, NULL); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); + + g_key_file_remove_key (keyfile, "backend", "reason", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + long_reason = "This reason is unreasonably long, it stretches over " + "more than twohundredfiftysix characters, which is really quite " + "long. Excessively so. The portal frontend will silently drop " + "reasons of this magnitude. If you can't express your reasons " + "concisely, you probably have no good reason in the first place " + "and are just waffling around."; + g_assert (g_utf8_strlen (long_reason, -1) > 256); + + got_info = 0; + xdp_portal_get_user_information (portal, NULL, long_reason, 0, NULL, account_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); + +} + +/* test that everything works as expected when the + * backend takes some time to send its response, as + * is to be expected from a real backend that presents + * dialogs to the user. + */ +void +test_account_delay (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + keyfile = g_key_file_new (); + g_key_file_set_string (keyfile, "account", "id", "test"); + g_key_file_set_string (keyfile, "account", "name", "Donald Duck"); + g_key_file_set_string (keyfile, "backend", "reason", "xx"); + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "account", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_get_user_information (portal, NULL, "xx", 0, NULL, account_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +/* Test that user cancellation works as expected. + * We simulate that the user cancels a hypothetical dialog, + * by telling the backend to return 1 as response code. + * And we check that we get the expected G_IO_ERROR_CANCELLED. + */ +void +test_account_cancel (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + keyfile = g_key_file_new (); + g_key_file_set_string (keyfile, "account", "id", "test"); + g_key_file_set_string (keyfile, "account", "name", "Donald Duck"); + g_key_file_set_string (keyfile, "backend", "reason", "xx"); + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 1); + g_key_file_set_integer (keyfile, "result", "response", 1); + + path = g_build_filename (outdir, "account", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_get_user_information (portal, NULL, "xx", 0, NULL, account_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +static gboolean +cancel_call (gpointer data) +{ + GCancellable *cancellable = data; + + g_debug ("cancel call"); + g_cancellable_cancel (cancellable); + + return G_SOURCE_REMOVE; +} + +/* Test that app-side cancellation works as expected. + * We cancel the cancellable while while the hypothetical + * dialog is up, and tell the backend that it should + * expect a Close call. We rely on the backend to + * verify that that call actually happened. + */ +void +test_account_close (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + g_autoptr(GCancellable) cancellable = NULL; + + keyfile = g_key_file_new (); + g_key_file_set_string (keyfile, "account", "id", "test"); + g_key_file_set_string (keyfile, "account", "name", "Donald Duck"); + g_key_file_set_string (keyfile, "backend", "reason", "xx"); + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_boolean (keyfile, "backend", "expect-close", 1); + g_key_file_set_integer (keyfile, "result", "response", 1); + + path = g_build_filename (outdir, "account", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + cancellable = g_cancellable_new (); + + got_info = 0; + xdp_portal_get_user_information (portal, NULL, "xx", 0, cancellable, account_cb, keyfile); + + g_timeout_add (100, cancel_call, cancellable); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +/* Test multiple requests in parallel */ +void +test_account_parallel (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + keyfile = g_key_file_new (); + + g_key_file_set_string (keyfile, "account", "id", "test"); + g_key_file_set_string (keyfile, "account", "name", "Donald Duck"); + g_key_file_set_string (keyfile, "account", "image", ""); + + g_key_file_set_string (keyfile, "backend", "reason", "test"); + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "account", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_get_user_information (portal, NULL, "test", 0, NULL, account_cb, keyfile); + xdp_portal_get_user_information (portal, NULL, "test", 0, NULL, account_cb, keyfile); + xdp_portal_get_user_information (portal, NULL, "test", 0, NULL, account_cb, keyfile); + + while (got_info < 3) + g_main_context_iteration (NULL, TRUE); +} + diff --git a/tests/account.h b/tests/account.h new file mode 100644 index 0000000..b2f61f2 --- /dev/null +++ b/tests/account.h @@ -0,0 +1,8 @@ +#pragma once + +void test_account_basic (void); +void test_account_delay (void); +void test_account_cancel (void); +void test_account_close (void); +void test_account_parallel (void); +void test_account_reason (void); diff --git a/tests/backend/access.c b/tests/backend/access.c new file mode 100644 index 0000000..0ba13c0 --- /dev/null +++ b/tests/backend/access.c @@ -0,0 +1,171 @@ +#include +#include +#include + +#include + +#include "xdp-impl-dbus.h" + +#include "request.h" +#include "access.h" + +typedef struct { + XdpDbusImplAccess *impl; + GDBusMethodInvocation *invocation; + Request *request; + GKeyFile *keyfile; + char *app_id; + char *title; + char *subtitle; + char *body; + GVariant *options; + guint timeout; +} AccessHandle; + +static void +access_handle_free (AccessHandle *handle) +{ + g_object_unref (handle->impl); + g_object_unref (handle->request); + g_key_file_unref (handle->keyfile); + g_free (handle->app_id); + g_free (handle->title); + g_free (handle->subtitle); + g_free (handle->body); + if (handle->timeout) + g_source_remove (handle->timeout); + + g_free (handle); +} + +static gboolean +send_response (gpointer data) +{ + AccessHandle *handle = data; + GVariantBuilder opt_builder; + int response; + + if (g_key_file_get_boolean (handle->keyfile, "backend", "expect-close", NULL)) + g_assert_not_reached (); + + response = g_key_file_get_integer (handle->keyfile, "backend", "response", NULL); + + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + + if (handle->request->exported) + request_unexport (handle->request); + + g_debug ("send response %d", response); + + xdp_dbus_impl_access_complete_access_dialog (handle->impl, + handle->invocation, + response, + g_variant_builder_end (&opt_builder)); + + handle->timeout = 0; + + access_handle_free (handle); + + return G_SOURCE_REMOVE; +} + +static gboolean +handle_close (XdpDbusImplRequest *object, + GDBusMethodInvocation *invocation, + AccessHandle *handle) +{ + GVariantBuilder opt_builder; + + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + g_debug ("AccessDialog handling Close"); + xdp_dbus_impl_access_complete_access_dialog (handle->impl, + handle->invocation, + 2, + g_variant_builder_end (&opt_builder)); + access_handle_free (handle); + + return FALSE; +} + +static gboolean +handle_access_dialog (XdpDbusImplAccess *object, + GDBusMethodInvocation *invocation, + const char *arg_handle, + const char *arg_app_id, + const char *arg_parent_window, + const char *arg_title, + const char *arg_subtitle, + const char *arg_body, + GVariant *arg_options) +{ + const char *sender; + const char *dir; + g_autofree char *path = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + int delay; + AccessHandle *handle; + g_autoptr(Request) request = NULL; + + g_debug ("Handling AccessDialog"); + + sender = g_dbus_method_invocation_get_sender (invocation); + + dir = g_getenv ("XDG_DATA_HOME"); + path = g_build_filename (dir, "access", NULL); + keyfile = g_key_file_new (); + g_key_file_load_from_file (keyfile, path, 0, &error); + g_assert_no_error (error); + + request = request_new (sender, arg_app_id, arg_handle); + + handle = g_new0 (AccessHandle, 1); + handle->impl = g_object_ref (object); + handle->invocation = invocation; + handle->request = g_object_ref (request); + handle->keyfile = g_key_file_ref (keyfile); + handle->app_id = g_strdup (arg_app_id); + handle->title = g_strdup (arg_title); + handle->subtitle = g_strdup (arg_subtitle); + handle->body = g_strdup (arg_body); + + g_signal_connect (request, "handle-close", G_CALLBACK (handle_close), handle); + + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + if (g_key_file_has_key (keyfile, "backend", "delay", NULL)) + delay = g_key_file_get_integer (keyfile, "backend", "delay", NULL); + else + delay = 200; + + g_debug ("delay %d", delay); + + if (delay == 0) + send_response (handle); + else + handle->timeout = g_timeout_add (delay, send_response, handle); + + return TRUE; +} + +void +access_init (GDBusConnection *connection, + const char *object_path) +{ + g_autoptr(GError) error = NULL; + GDBusInterfaceSkeleton *helper; + + helper = G_DBUS_INTERFACE_SKELETON (xdp_dbus_impl_access_skeleton_new ()); + + g_signal_connect (helper, "handle-access-dialog", G_CALLBACK (handle_access_dialog), NULL); + + if (!g_dbus_interface_skeleton_export (helper, connection, object_path, &error)) + { + g_error ("Failed to export %s skeleton: %s\n", + g_dbus_interface_skeleton_get_info (helper)->name, + error->message); + exit (1); + } + + g_debug ("providing %s at %s", g_dbus_interface_skeleton_get_info (helper)->name, object_path); +} diff --git a/tests/backend/access.h b/tests/backend/access.h new file mode 100644 index 0000000..fb18711 --- /dev/null +++ b/tests/backend/access.h @@ -0,0 +1,3 @@ +#pragma once + +void access_init (GDBusConnection *connection, const char *object_path); diff --git a/tests/backend/account.c b/tests/backend/account.c new file mode 100644 index 0000000..19f8ff1 --- /dev/null +++ b/tests/backend/account.c @@ -0,0 +1,186 @@ +#include +#include + +#include + +#include "xdp-impl-dbus.h" + +#include "request.h" +#include "account.h" + +typedef struct { + XdpDbusImplAccount *impl; + GDBusMethodInvocation *invocation; + Request *request; + GKeyFile *keyfile; + char *app_id; + char *reason; + guint timeout; +} AccountDialogHandle; + +static void +account_dialog_handle_free (AccountDialogHandle *handle) +{ + g_object_unref (handle->impl); + g_object_unref (handle->request); + g_key_file_unref (handle->keyfile); + g_free (handle->app_id); + g_free (handle->reason); + if (handle->timeout) + g_source_remove (handle->timeout); + + g_free (handle); +} + +static gboolean +send_response (gpointer data) +{ + AccountDialogHandle *handle = data; + GVariantBuilder opt_builder; + g_autofree char *reason = NULL; + g_autofree char *id = NULL; + g_autofree char *name = NULL; + g_autofree char *image = NULL; + g_autoptr(GError) error = NULL; + int response; + + if (g_key_file_get_boolean (handle->keyfile, "backend", "expect-close", NULL)) + g_assert_not_reached (); + + reason = g_key_file_get_string (handle->keyfile, "backend", "reason", &error); + id = g_key_file_get_string (handle->keyfile, "account", "id", NULL); + name = g_key_file_get_string (handle->keyfile, "account", "name", NULL); + image = g_key_file_get_string (handle->keyfile, "account", "image", NULL); + + response = g_key_file_get_integer (handle->keyfile, "backend", "response", NULL); + + if (g_strcmp0 (handle->reason, reason) != 0) + { + g_dbus_method_invocation_return_error (handle->invocation, G_IO_ERROR, G_IO_ERROR_FAILED, "Unexpected reason: '%s' != '%s'", reason, handle->reason); + return TRUE; + } + + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + if (id) + g_variant_builder_add (&opt_builder, "{sv}", "id", g_variant_new_string (id)); + if (name) + g_variant_builder_add (&opt_builder, "{sv}", "name", g_variant_new_string (name)); + if (image) + g_variant_builder_add (&opt_builder, "{sv}", "image", g_variant_new_string (image)); + + if (handle->request->exported) + request_unexport (handle->request); + + g_debug ("send response %d", response); + + xdp_dbus_impl_account_complete_get_user_information (handle->impl, + handle->invocation, + response, + g_variant_builder_end (&opt_builder)); + + handle->timeout = 0; + + account_dialog_handle_free (handle); + + return G_SOURCE_REMOVE; +} + +static gboolean +handle_close (XdpDbusImplRequest *object, + GDBusMethodInvocation *invocation, + AccountDialogHandle *handle) +{ + GVariantBuilder opt_builder; + + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + g_debug ("send response 2"); + xdp_dbus_impl_account_complete_get_user_information (handle->impl, + handle->invocation, + 2, + g_variant_builder_end (&opt_builder)); + account_dialog_handle_free (handle); + + return FALSE; +} + + +static gboolean +handle_get_user_information (XdpDbusImplAccount *object, + GDBusMethodInvocation *invocation, + const char *arg_handle, + const char *arg_app_id, + const char *arg_parent_window, + GVariant *arg_options) +{ + const char *sender; + const char *dir; + g_autofree char *path = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + int delay; + AccountDialogHandle *handle; + const char *reason = NULL; + g_autoptr(Request) request = NULL; + + g_debug ("Handling GetUserInformation"); + + sender = g_dbus_method_invocation_get_sender (invocation); + + dir = g_getenv ("XDG_DATA_HOME"); + path = g_build_filename (dir, "account", NULL); + keyfile = g_key_file_new (); + g_key_file_load_from_file (keyfile, path, 0, &error); + g_assert_no_error (error); + + request = request_new (sender, arg_app_id, arg_handle); + + g_variant_lookup (arg_options, "reason", "&s", &reason); + + handle = g_new0 (AccountDialogHandle, 1); + handle->impl = g_object_ref (object); + handle->invocation = invocation; + handle->request = g_object_ref (request); + handle->keyfile = g_key_file_ref (keyfile); + handle->app_id = g_strdup (arg_app_id); + handle->reason = g_strdup (reason); + + g_signal_connect (request, "handle-close", G_CALLBACK (handle_close), handle); + + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + if (g_key_file_has_key (keyfile, "backend", "delay", NULL)) + delay = g_key_file_get_integer (keyfile, "backend", "delay", NULL); + else + delay = 200; + + g_debug ("delay %d", delay); + + if (delay == 0) + send_response (handle); + else + handle->timeout = g_timeout_add (delay, send_response, handle); + + return TRUE; +} + +void +account_init (GDBusConnection *connection, + const char *object_path) +{ + g_autoptr(GError) error = NULL; + GDBusInterfaceSkeleton *helper; + + helper = G_DBUS_INTERFACE_SKELETON (xdp_dbus_impl_account_skeleton_new ()); + + g_signal_connect (helper, "handle-get-user-information", G_CALLBACK (handle_get_user_information), NULL); + + if (!g_dbus_interface_skeleton_export (helper, connection, object_path, &error)) + { + g_error ("Failed to export %s skeleton: %s\n", + g_dbus_interface_skeleton_get_info (helper)->name, + error->message); + exit (1); + } + + g_debug ("providing %s at %s", g_dbus_interface_skeleton_get_info (helper)->name, object_path); +} diff --git a/tests/backend/account.h b/tests/backend/account.h new file mode 100644 index 0000000..120cf9e --- /dev/null +++ b/tests/backend/account.h @@ -0,0 +1,3 @@ +#pragma once + +void account_init (GDBusConnection *connection, const char *object_path); diff --git a/tests/backend/appchooser.c b/tests/backend/appchooser.c new file mode 100644 index 0000000..0d4ac65 --- /dev/null +++ b/tests/backend/appchooser.c @@ -0,0 +1,183 @@ +#include +#include +#include + +#include + +#include "xdp-impl-dbus.h" + +#include "request.h" +#include "appchooser.h" + +typedef struct { + XdpDbusImplAppChooser *impl; + GDBusMethodInvocation *invocation; + Request *request; + GKeyFile *keyfile; + char *app_id; + guint timeout; + char **choices; + GVariant *options; +} AppChooserHandle; + +static void +app_chooser_handle_free (AppChooserHandle *handle) +{ + g_object_unref (handle->impl); + g_object_unref (handle->request); + g_key_file_unref (handle->keyfile); + g_free (handle->app_id); + if (handle->timeout) + g_source_remove (handle->timeout); + g_strfreev (handle->choices); + g_variant_unref (handle->options); + + g_free (handle); +} + +static gboolean +send_response (gpointer data) +{ + AppChooserHandle *handle = data; + GVariantBuilder opt_builder; + int response; + + if (g_key_file_get_boolean (handle->keyfile, "backend", "expect-close", NULL)) + g_assert_not_reached (); + + response = g_key_file_get_integer (handle->keyfile, "backend", "response", NULL); + + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + + if (handle->request->exported) + request_unexport (handle->request); + + if (response == 0) + { + if (handle->choices[0]) + { + g_debug ("choice: %s", handle->choices[0]); + g_variant_builder_add (&opt_builder, "{sv}", "choice", g_variant_new_string (handle->choices[0])); + } + } + + g_debug ("send response %d", response); + + xdp_dbus_impl_app_chooser_complete_choose_application (handle->impl, + handle->invocation, + response, + g_variant_builder_end (&opt_builder)); + + handle->timeout = 0; + + app_chooser_handle_free (handle); + + return G_SOURCE_REMOVE; +} + +static gboolean +handle_close (XdpDbusImplRequest *object, + GDBusMethodInvocation *invocation, + AppChooserHandle *handle) +{ + GVariantBuilder opt_builder; + + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + xdp_dbus_impl_app_chooser_complete_choose_application (handle->impl, + handle->invocation, + 2, + g_variant_builder_end (&opt_builder)); + app_chooser_handle_free (handle); + + return FALSE; +} + + +static gboolean +handle_choose_application (XdpDbusImplAppChooser *object, + GDBusMethodInvocation *invocation, + const char *arg_handle, + const char *arg_app_id, + const char *arg_parent_window, + const char * const *arg_choices, + GVariant *arg_options) +{ + const char *sender; + const char *dir; + g_autofree char *path = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + int delay; + AppChooserHandle *handle; + g_autoptr(Request) request = NULL; + + g_debug ("Handling ChooseApplication"); + + sender = g_dbus_method_invocation_get_sender (invocation); + + dir = g_getenv ("XDG_DATA_HOME"); + path = g_build_filename (dir, "appchooser", NULL); + keyfile = g_key_file_new (); + g_key_file_load_from_file (keyfile, path, 0, &error); + g_assert_no_error (error); + + if (g_key_file_has_key (keyfile, "backend", "expect-no-call", NULL)) + { + g_dbus_method_invocation_return_error (invocation, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Did not expect ChooseApplication to be called here"); + return TRUE; /* handled */ + } + + request = request_new (sender, arg_app_id, arg_handle); + + handle = g_new0 (AppChooserHandle, 1); + handle->impl = g_object_ref (object); + handle->invocation = invocation; + handle->request = g_object_ref (request); + handle->keyfile = g_key_file_ref (keyfile); + handle->app_id = g_strdup (arg_app_id); + handle->choices = g_strdupv ((char **)arg_choices); + handle->options = g_variant_ref (arg_options); + + g_signal_connect (request, "handle-close", G_CALLBACK (handle_close), handle); + + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + if (g_key_file_has_key (keyfile, "backend", "delay", NULL)) + delay = g_key_file_get_integer (keyfile, "backend", "delay", NULL); + else + delay = 200; + + g_debug ("delay %d", delay); + + if (delay == 0) + send_response (handle); + else + handle->timeout = g_timeout_add (delay, send_response, handle); + + return TRUE; +} + +void +appchooser_init (GDBusConnection *connection, + const char *object_path) +{ + g_autoptr(GError) error = NULL; + GDBusInterfaceSkeleton *helper; + + helper = G_DBUS_INTERFACE_SKELETON (xdp_dbus_impl_app_chooser_skeleton_new ()); + + g_signal_connect (helper, "handle-choose-application", G_CALLBACK (handle_choose_application), NULL); + + if (!g_dbus_interface_skeleton_export (helper, connection, object_path, &error)) + { + g_error ("Failed to export %s skeleton: %s\n", + g_dbus_interface_skeleton_get_info (helper)->name, + error->message); + exit (1); + } + + g_debug ("providing %s at %s", g_dbus_interface_skeleton_get_info (helper)->name, object_path); +} diff --git a/tests/backend/appchooser.h b/tests/backend/appchooser.h new file mode 100644 index 0000000..b0ee145 --- /dev/null +++ b/tests/backend/appchooser.h @@ -0,0 +1,3 @@ +#pragma once + +void appchooser_init (GDBusConnection *connection, const char *object_path); diff --git a/tests/backend/background.c b/tests/backend/background.c new file mode 100644 index 0000000..09e10d8 --- /dev/null +++ b/tests/backend/background.c @@ -0,0 +1,99 @@ +#include +#include +#include + +#include + +#include "xdp-impl-dbus.h" + +#include "request.h" +#include "background.h" + + +static gboolean +handle_get_app_state (XdpDbusImplBackground *object, + GDBusMethodInvocation *invocation) +{ + GVariantBuilder builder; + + g_debug ("background: handle GetAppState"); + + g_variant_builder_init (&builder, G_VARIANT_TYPE_VARDICT); + xdp_dbus_impl_background_complete_get_app_state (object, + invocation, + g_variant_builder_end (&builder)); + + return TRUE; +} + +static gboolean +handle_notify_background (XdpDbusImplBackground *object, + GDBusMethodInvocation *invocation, + const char *arg_handle, + const char *arg_app_id, + const char *arg_name) +{ + GVariantBuilder opt_builder; + + g_debug ("background: handle NotifyBackground"); + + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + xdp_dbus_impl_background_complete_notify_background (object, + invocation, + 2, + g_variant_builder_end (&opt_builder)); + + return TRUE; +} + +static gboolean +handle_enable_autostart (XdpDbusImplBackground *object, + GDBusMethodInvocation *invocation, + const char *arg_app_id, + gboolean arg_enable, + const char * const *arg_commandline, + guint arg_flags) +{ + const char *dir; + g_autofree char *path = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + + g_debug ("background: handle EnableAutostart"); + + dir = g_getenv ("XDG_DATA_HOME"); + path = g_build_filename (dir, "background", NULL); + keyfile = g_key_file_new (); + g_key_file_load_from_file (keyfile, path, 0, &error); + g_assert_no_error (error); + + g_assert (arg_enable == g_key_file_get_boolean (keyfile, "background", "autostart", NULL)); + + xdp_dbus_impl_background_complete_enable_autostart (object, invocation, TRUE); + + return TRUE; +} + +void +background_init (GDBusConnection *connection, + const char *object_path) +{ + g_autoptr(GError) error = NULL; + GDBusInterfaceSkeleton *helper; + + helper = G_DBUS_INTERFACE_SKELETON (xdp_dbus_impl_background_skeleton_new ()); + + g_signal_connect (helper, "handle-get-app-state", G_CALLBACK (handle_get_app_state), NULL); + g_signal_connect (helper, "handle-notify-background", G_CALLBACK (handle_notify_background), NULL); + g_signal_connect (helper, "handle-enable-autostart", G_CALLBACK (handle_enable_autostart), NULL); + + if (!g_dbus_interface_skeleton_export (helper, connection, object_path, &error)) + { + g_error ("Failed to export %s skeleton: %s\n", + g_dbus_interface_skeleton_get_info (helper)->name, + error->message); + exit (1); + } + + g_debug ("providing %s at %s", g_dbus_interface_skeleton_get_info (helper)->name, object_path); +} diff --git a/tests/backend/background.h b/tests/backend/background.h new file mode 100644 index 0000000..21bbea2 --- /dev/null +++ b/tests/backend/background.h @@ -0,0 +1,3 @@ +#pragma once + +void background_init (GDBusConnection *connection, const char *object_path); diff --git a/tests/backend/email.c b/tests/backend/email.c new file mode 100644 index 0000000..1b62e3a --- /dev/null +++ b/tests/backend/email.c @@ -0,0 +1,212 @@ +#include "config.h" +#include + +#include + +#include "xdp-impl-dbus.h" +#include "tests/glib-backports.h" + +#include "email.h" +#include "request.h" + +typedef struct { + XdpDbusImplEmail *impl; + GDBusMethodInvocation *invocation; + Request *request; + GKeyFile *keyfile; + char *app_id; + GVariant *options; + guint timeout; +} EmailHandle; + +static void +email_handle_free (EmailHandle *handle) +{ + g_object_unref (handle->impl); + g_object_unref (handle->request); + g_key_file_unref (handle->keyfile); + g_free (handle->app_id); + g_variant_unref (handle->options); + if (handle->timeout) + g_source_remove (handle->timeout); + + g_free (handle); +} + +static gboolean +send_response (gpointer data) +{ + EmailHandle *handle = data; + GVariantBuilder opt_builder; + const char *address = NULL; + const char *subject = NULL; + const char *body = NULL; + const char *no_att[1] = { NULL }; + const char **attachments = no_att; + char *s; + int response; + const char * const *addresses; + const char * const *cc; + const char * const *bcc; + char **strv; + + g_variant_lookup (handle->options, "address", "&s", &address); + g_variant_lookup (handle->options, "subject", "&s", &subject); + g_variant_lookup (handle->options, "body", "&s", &body); + g_variant_lookup (handle->options, "attachments", "^a&s", &attachments); + g_variant_lookup (handle->options, "addresses", "^a&s", &addresses); + g_variant_lookup (handle->options, "cc", "^a&s", &cc); + g_variant_lookup (handle->options, "bcc", "^a&s", &bcc); + + if (g_key_file_get_boolean (handle->keyfile, "backend", "expect-close", NULL)) + g_assert_not_reached (); + + s = g_key_file_get_string (handle->keyfile, "input", "subject", NULL); + g_assert_cmpstr (s, ==, subject); + g_free (s); + s = g_key_file_get_string (handle->keyfile, "input", "body", NULL); + g_assert_cmpstr (s, ==, body); + g_free (s); + + strv = g_key_file_get_string_list (handle->keyfile, "input", "addresses", NULL, NULL); + if (strv) + { + g_assert (addresses != NULL); + g_assert_true (g_strv_equal ((const char * const *)strv, addresses)); + g_strfreev (strv); + } + + strv = g_key_file_get_string_list (handle->keyfile, "input", "cc", NULL, NULL); + if (strv) + { + g_assert (addresses != NULL); + g_assert_true (g_strv_equal ((const char * const *)strv, cc)); + g_strfreev (strv); + } + + strv = g_key_file_get_string_list (handle->keyfile, "input", "bcc", NULL, NULL); + if (strv) + { + g_assert (addresses != NULL); + g_assert_true (g_strv_equal ((const char * const *)strv, bcc)); + g_strfreev (strv); + } + + /* fixme: attachments */ + + response = g_key_file_get_integer (handle->keyfile, "backend", "response", NULL); + + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + + if (handle->request->exported) + request_unexport (handle->request); + + g_debug ("send response %d", response); + + xdp_dbus_impl_email_complete_compose_email (handle->impl, + handle->invocation, + response, + g_variant_builder_end (&opt_builder)); + + handle->timeout = 0; + + email_handle_free (handle); + + return G_SOURCE_REMOVE; +} + +static gboolean +handle_close (XdpDbusImplRequest *object, + GDBusMethodInvocation *invocation, + EmailHandle *handle) +{ + GVariantBuilder opt_builder; + + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + g_debug ("send response 2"); + xdp_dbus_impl_email_complete_compose_email (handle->impl, + handle->invocation, + 2, + g_variant_builder_end (&opt_builder)); + email_handle_free (handle); + + return FALSE; +} + +static gboolean +handle_compose_email (XdpDbusImplEmail *object, + GDBusMethodInvocation *invocation, + const char *arg_handle, + const char *arg_app_id, + const char *arg_parent_window, + GVariant *arg_options) +{ + g_autoptr(Request) request = NULL; + const char *sender; + g_autoptr(GError) error = NULL; + const char *dir; + g_autofree char *path = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + EmailHandle *handle; + int delay; + + g_debug ("Handling ComposeEmail"); + + sender = g_dbus_method_invocation_get_sender (invocation); + + dir = g_getenv ("XDG_DATA_HOME"); + path = g_build_filename (dir, "email", NULL); + keyfile = g_key_file_new (); + g_key_file_load_from_file (keyfile, path, 0, &error); + g_assert_no_error (error); + + request = request_new (sender, arg_app_id, arg_handle); + + handle = g_new (EmailHandle, 1); + handle->impl = g_object_ref (object); + handle->invocation = invocation; + handle->request = g_object_ref (request); + handle->keyfile = g_key_file_ref (keyfile); + handle->app_id = g_strdup (arg_app_id); + handle->options = g_variant_ref (arg_options); + + g_signal_connect (request, "handle-close", G_CALLBACK (handle_close), handle); + + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + if (g_key_file_has_key (keyfile, "backend", "delay", NULL)) + delay = g_key_file_get_integer (keyfile, "backend", "delay", NULL); + else + delay = 200; + + g_debug ("delay %d", delay); + + if (delay == 0) + send_response (handle); + else + handle->timeout = g_timeout_add (delay, send_response, handle); + + return TRUE; +} + +void +email_init (GDBusConnection *bus, + const char *object_path) +{ + g_autoptr(GError) error = NULL; + GDBusInterfaceSkeleton *helper; + + helper = G_DBUS_INTERFACE_SKELETON (xdp_dbus_impl_email_skeleton_new ()); + + g_signal_connect (helper, "handle-compose-email", G_CALLBACK (handle_compose_email), NULL); + + if (!g_dbus_interface_skeleton_export (helper, bus, object_path, &error)) + { + g_error ("Failed to export %s skeleton: %s\n", + g_dbus_interface_skeleton_get_info (helper)->name, + error->message); + exit (1); + } + + g_debug ("providing %s at %s", g_dbus_interface_skeleton_get_info (helper)->name, object_path); +} diff --git a/tests/backend/email.h b/tests/backend/email.h new file mode 100644 index 0000000..0dfe7bc --- /dev/null +++ b/tests/backend/email.h @@ -0,0 +1,3 @@ +#pragma once + +void email_init (GDBusConnection *bus, const char *object_path); diff --git a/tests/backend/filechooser.c b/tests/backend/filechooser.c new file mode 100644 index 0000000..788b9e1 --- /dev/null +++ b/tests/backend/filechooser.c @@ -0,0 +1,243 @@ +#include +#include +#include +#include + +#include + +#include "xdp-impl-dbus.h" + +#include "request.h" +#include "account.h" + +typedef struct { + XdpDbusImplFileChooser *impl; + GDBusMethodInvocation *invocation; + Request *request; + GKeyFile *keyfile; + char *app_id; + char *title; + GVariant *options; + guint timeout; +} FileChooserHandle; + +static void +file_chooser_handle_free (FileChooserHandle *handle) +{ + g_object_unref (handle->impl); + g_object_unref (handle->request); + g_key_file_unref (handle->keyfile); + g_free (handle->app_id); + if (handle->timeout) + g_source_remove (handle->timeout); + g_free (handle->title); + g_variant_unref (handle->options); + g_free (handle); +} + +static gboolean +send_response (gpointer data) +{ + FileChooserHandle *handle = data; + GVariantBuilder opt_builder; + g_autoptr(GVariant) current_filter = NULL; + g_autoptr(GVariant) choices = NULL; + g_autoptr(GVariant) filters = NULL; + g_autofree char *filters_string = NULL; + g_autofree char *current_filter_string = NULL; + g_autofree char *choices_string = NULL; + int response; + + if (g_key_file_get_boolean (handle->keyfile, "backend", "expect-close", NULL)) + g_assert_not_reached (); + + response = g_key_file_get_integer (handle->keyfile, "backend", "response", NULL); + + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + + g_variant_lookup (handle->options, "filters", "@a(sa(us))", &filters); + filters_string = g_key_file_get_string (handle->keyfile, "backend", "filters", NULL); + if (filters_string) + { + g_autoptr(GVariant) expected = NULL; + g_assert_nonnull (filters); + expected = g_variant_parse (G_VARIANT_TYPE ("a(sa(us))"), filters_string, NULL, NULL, NULL); + g_assert (g_variant_equal (filters, expected)); + } + else + { + g_assert_null (filters); + } + + g_variant_lookup (handle->options, "current_filter", "@(sa(us))", ¤t_filter); + current_filter_string = g_key_file_get_string (handle->keyfile, "backend", "current_filter", NULL); + if (current_filter_string) + { + g_autoptr(GVariant) expected = NULL; + g_assert_nonnull (current_filter); + expected = g_variant_parse (G_VARIANT_TYPE ("(sa(us))"), current_filter_string, NULL, NULL, NULL); + g_assert (g_variant_equal (current_filter, expected)); + } + else + { + g_assert_null (current_filter); + } + + g_variant_lookup (handle->options, "choices", "@a(ssa(ss)s)", &choices); + choices_string = g_key_file_get_string (handle->keyfile, "backend", "choices", NULL); + if (choices_string) + { + g_autoptr(GVariant) expected = NULL; + g_assert_nonnull (choices); + expected = g_variant_parse (G_VARIANT_TYPE ("a(ssa(ss)s)"), choices_string, NULL, NULL, NULL); + g_assert (g_variant_equal (choices, expected)); + } + else + { + g_assert_null (choices); + } + + if (response == 0) + { + g_auto(GStrv) uris = NULL; + g_autofree char *chosen_string = NULL; + + uris = g_key_file_get_string_list (handle->keyfile, "result", "uris", NULL, NULL); + g_variant_builder_add (&opt_builder, "{sv}", "uris", g_variant_new_strv ((const char * const *)uris, -1)); + + chosen_string = g_key_file_get_string (handle->keyfile, "result", "choices", NULL); + if (chosen_string) + { + g_autoptr(GVariant) chosen = NULL; + chosen = g_variant_parse (G_VARIANT_TYPE ("a(ss)"), chosen_string, NULL, NULL, NULL); + g_variant_builder_add (&opt_builder, "{sv}", "choices", chosen); + } + } + + if (handle->request->exported) + request_unexport (handle->request); + + g_debug ("send response %d", response); + + if (strcmp (g_dbus_method_invocation_get_method_name (handle->invocation), "OpenFile") == 0) + xdp_dbus_impl_file_chooser_complete_open_file (handle->impl, + handle->invocation, + response, + g_variant_builder_end (&opt_builder)); + else + xdp_dbus_impl_file_chooser_complete_save_file (handle->impl, + handle->invocation, + response, + g_variant_builder_end (&opt_builder)); + + handle->timeout = 0; + + file_chooser_handle_free (handle); + + return G_SOURCE_REMOVE; +} + +static gboolean +handle_close (XdpDbusImplRequest *object, + GDBusMethodInvocation *invocation, + FileChooserHandle *handle) +{ + GVariantBuilder opt_builder; + + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + g_debug ("send response 2"); + if (strcmp (g_dbus_method_invocation_get_method_name (handle->invocation), "OpenFile") == 0) + xdp_dbus_impl_file_chooser_complete_open_file (handle->impl, + handle->invocation, + 2, + g_variant_builder_end (&opt_builder)); + else + xdp_dbus_impl_file_chooser_complete_save_file (handle->impl, + handle->invocation, + 2, + g_variant_builder_end (&opt_builder)); + file_chooser_handle_free (handle); + + return FALSE; +} + +static gboolean +handle_open_file (XdpDbusImplFileChooser *object, + GDBusMethodInvocation *invocation, + const char *arg_handle, + const char *arg_app_id, + const char *arg_parent_window, + const char *arg_title, + GVariant *arg_options) +{ + const char *sender; + const char *dir; + g_autofree char *path = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + int delay; + FileChooserHandle *handle; + g_autoptr(Request) request = NULL; + + g_debug ("Handling %s", g_dbus_method_invocation_get_method_name (invocation)); + + sender = g_dbus_method_invocation_get_sender (invocation); + + dir = g_getenv ("XDG_DATA_HOME"); + path = g_build_filename (dir, "filechooser", NULL); + keyfile = g_key_file_new (); + g_key_file_load_from_file (keyfile, path, 0, &error); + g_assert_no_error (error); + + request = request_new (sender, arg_app_id, arg_handle); + + handle = g_new0 (FileChooserHandle, 1); + handle->impl = g_object_ref (object); + handle->invocation = invocation; + handle->request = g_object_ref (request); + handle->keyfile = g_key_file_ref (keyfile); + handle->app_id = g_strdup (arg_app_id); + handle->title = g_strdup (arg_title); + handle->options = g_variant_ref (arg_options); + + g_signal_connect (request, "handle-close", G_CALLBACK (handle_close), handle); + + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + if (g_key_file_has_key (keyfile, "backend", "delay", NULL)) + delay = g_key_file_get_integer (keyfile, "backend", "delay", NULL); + else + delay = 200; + + g_debug ("delay %d", delay); + + if (delay == 0) + send_response (handle); + else + handle->timeout = g_timeout_add (delay, send_response, handle); + + return TRUE; +} + +void +file_chooser_init (GDBusConnection *connection, + const char *object_path) +{ + g_autoptr(GError) error = NULL; + GDBusInterfaceSkeleton *helper; + + helper = G_DBUS_INTERFACE_SKELETON (xdp_dbus_impl_file_chooser_skeleton_new ()); + + g_signal_connect (helper, "handle-open-file", G_CALLBACK (handle_open_file), NULL); + g_signal_connect (helper, "handle-save-file", G_CALLBACK (handle_open_file), NULL); + + if (!g_dbus_interface_skeleton_export (helper, connection, object_path, &error)) + { + g_error ("Failed to export %s skeleton: %s\n", + g_dbus_interface_skeleton_get_info (helper)->name, + error->message); + exit (1); + } + + g_debug ("providing %s at %s", g_dbus_interface_skeleton_get_info (helper)->name, object_path); +} diff --git a/tests/backend/filechooser.h b/tests/backend/filechooser.h new file mode 100644 index 0000000..da5d430 --- /dev/null +++ b/tests/backend/filechooser.h @@ -0,0 +1,3 @@ +#pragma once + +void file_chooser_init (GDBusConnection *connection, const char *object_path); diff --git a/tests/backend/inhibit.c b/tests/backend/inhibit.c new file mode 100644 index 0000000..868c695 --- /dev/null +++ b/tests/backend/inhibit.c @@ -0,0 +1,460 @@ +#include +#include +#include + +#include + +#include "xdp-impl-dbus.h" + +#include "request.h" +#include "session.h" +#include "inhibit.h" + +static GDBusInterfaceSkeleton *inhibit; + +typedef struct { + XdpDbusImplInhibit *impl; + GDBusMethodInvocation *invocation; + Request *request; + GKeyFile *keyfile; + char *app_id; + guint flags; + guint close_id; + int timeout; +} InhibitHandle; + +static void +inhibit_handle_free (InhibitHandle *handle) +{ + g_object_unref (handle->impl); + if (handle->request) + g_object_unref (handle->request); + g_key_file_unref (handle->keyfile); + g_free (handle->app_id); + + if (handle->timeout) + g_source_remove (handle->timeout); + + g_free (handle); +} + +static gboolean +handle_close (Request *object, + GDBusMethodInvocation *invocation, + gpointer data) +{ + InhibitHandle *handle = g_object_get_data (G_OBJECT (object), "handle"); + + if (object->exported) + request_unexport (object); + + xdp_dbus_impl_request_complete_close (XDP_DBUS_IMPL_REQUEST (object), invocation); + + g_debug ("Handling Close"); + + if (handle) + inhibit_handle_free (handle); + else + g_object_unref (object); + + return TRUE; +} + +static gboolean +send_response (gpointer data) +{ + InhibitHandle *handle = data; + int response; + + if (g_key_file_get_boolean (handle->keyfile, "backend", "expect-close", NULL)) + g_assert_not_reached (); + + response = g_key_file_get_integer (handle->keyfile, "backend", "response", NULL); + + if (response == 0) + { + xdp_dbus_impl_inhibit_complete_inhibit (handle->impl, handle->invocation); + g_object_set_data (G_OBJECT (handle->request), "handle", NULL); + handle->request = NULL; + } + else + g_dbus_method_invocation_return_error (handle->invocation, G_IO_ERROR, G_IO_ERROR_FAILED, "Canceled"); + + handle->timeout = 0; + + inhibit_handle_free (handle); + + return G_SOURCE_REMOVE; +} + +static gboolean +handle_inhibit (XdpDbusImplInhibit *object, + GDBusMethodInvocation *invocation, + const char *arg_handle, + const char *arg_app_id, + const char *arg_parent_window, + guint arg_flags, + GVariant *arg_options) +{ + const char *sender; + const char *dir; + g_autofree char *path = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + InhibitHandle *handle; + g_autoptr(Request) request = NULL; + int delay; + + g_debug ("Handling Inhibit"); + + sender = g_dbus_method_invocation_get_sender (invocation); + + dir = g_getenv ("XDG_DATA_HOME"); + path = g_build_filename (dir, "inhibit", NULL); + keyfile = g_key_file_new (); + g_key_file_load_from_file (keyfile, path, 0, &error); + g_assert_no_error (error); + + g_assert_cmpuint (arg_flags, ==, g_key_file_get_integer (keyfile, "inhibit", "flags", NULL)); + + request = request_new (sender, arg_app_id, arg_handle); + + handle = g_new0 (InhibitHandle, 1); + handle->impl = g_object_ref (object); + handle->invocation = invocation; + handle->request = g_object_ref (request); + handle->keyfile = g_key_file_ref (keyfile); + handle->app_id = g_strdup (arg_app_id); + handle->flags = arg_flags; + + g_object_set_data (G_OBJECT (request), "handle", handle); + handle->close_id = g_signal_connect (request, "handle-close", G_CALLBACK (handle_close), NULL); + + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + + if (g_key_file_has_key (keyfile, "backend", "delay", NULL)) + delay = g_key_file_get_integer (keyfile, "backend", "delay", NULL); + else + delay = 200; + + g_debug ("delay %d", delay); + + if (delay == 0) + send_response (handle); + else + handle->timeout = g_timeout_add (delay, send_response, handle); + + return TRUE; +} + +typedef enum { + UNKNOWN = 0, + RUNNING = 1, + QUERY_END = 2, + ENDING = 3 +} SessionState; + +static SessionState session_state = RUNNING; +static gboolean screensaver_active = FALSE; +static guint query_end_timeout; +static GList *active_sessions = NULL; + +static void +emit_state_changed (Session *session) +{ + GVariantBuilder state; + + g_debug ("Emitting StateChanged for session %s", session->id); + + g_variant_builder_init (&state, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add (&state, "{sv}", "screensaver-active", g_variant_new_boolean (screensaver_active)); + g_variant_builder_add (&state, "{sv}", "session-state", g_variant_new_uint32 (session_state)); + g_signal_emit_by_name (inhibit, "state-changed", session->id, g_variant_builder_end (&state)); +} + +typedef struct +{ + Session parent; + gboolean pending_query_end_response; +} InhibitSession; + +typedef struct _InhibitSessionClass +{ + SessionClass parent_class; +} InhibitSessionClass; + +GType inhibit_session_get_type (void); +G_DEFINE_TYPE (InhibitSession, inhibit_session, session_get_type ()) + +static void +global_set_pending_query_end_response (gboolean pending) +{ + GList *l; + + for (l = active_sessions; l; l = l->next) + { + InhibitSession *session = (InhibitSession *)l->data; + session->pending_query_end_response = pending; + } +} + +static gboolean +global_get_pending_query_end_response (void) +{ + GList *l; + + for (l = active_sessions; l; l = l->next) + { + InhibitSession *session = (InhibitSession *)l->data; + if (session->pending_query_end_response) + return TRUE; + } + + return FALSE; +} + +static void +inhibit_session_close (Session *session) +{ + InhibitSession *inhibit_session = (InhibitSession *)session; + + g_debug ("Closing inhibit session %s", ((Session *)inhibit_session)->id); + + active_sessions = g_list_remove (active_sessions, session); +} + +static void +inhibit_session_finalize (GObject *object) +{ + G_OBJECT_CLASS (inhibit_session_parent_class)->finalize (object); +} + +static void +inhibit_session_init (InhibitSession *inhibit_session) +{ +} + +static void +inhibit_session_class_init (InhibitSessionClass *klass) +{ + GObjectClass *gobject_class; + SessionClass *session_class; + + gobject_class = (GObjectClass *)klass; + gobject_class->finalize = inhibit_session_finalize; + + session_class = (SessionClass *)klass; + session_class->close = inhibit_session_close; +} + +static InhibitSession * +inhibit_session_new (const char *app_id, + const char *session_handle) +{ + InhibitSession *inhibit_session; + + g_debug ("Creating inhibit session %s", session_handle); + + inhibit_session = g_object_new (inhibit_session_get_type (), + "id", session_handle, + NULL); + + active_sessions = g_list_prepend (active_sessions, inhibit_session); + + return inhibit_session; +} + +static void +global_emit_state_changed (void) +{ + GList *l; + + for (l = active_sessions; l; l = l->next) + emit_state_changed ((Session *)l->data); +} + +static void +set_session_state (SessionState state) +{ + const char *names[] = { + "Unknown", "Running", "Query-end", "Ending" + }; + + g_debug ("Session state now: %s", names[state]); + + session_state = state; + + global_emit_state_changed (); +} + +static void global_set_pending_query_end_response (gboolean pending); +static gboolean global_get_pending_query_end_response (void); + +static void +stop_waiting_for_query_end_response (gboolean send_response) +{ + g_debug ("Stop waiting for QueryEndResponse calls"); + + if (query_end_timeout != 0) + { + g_source_remove (query_end_timeout); + query_end_timeout = 0; + } + + global_set_pending_query_end_response (FALSE); +} + +static gboolean +query_end_response (gpointer data) +{ + g_debug ("1 second wait is over"); + + stop_waiting_for_query_end_response (TRUE); + + return G_SOURCE_REMOVE; +} + +static void +wait_for_query_end_response (gpointer data) +{ + if (query_end_timeout != 0) + return; /* we're already waiting */ + + g_debug ("Waiting for up to 1 second for QueryEndResponse calls"); + + query_end_timeout = g_timeout_add (1000, query_end_response, data); + + global_set_pending_query_end_response (TRUE); +} + +static void +maybe_send_quit_response (void) +{ + if (query_end_timeout == 0) + return; + + if (global_get_pending_query_end_response ()) + return; + + g_debug ("No more pending QueryEndResponse calls"); + + stop_waiting_for_query_end_response (TRUE); +} + +static gboolean +change_session_state (gpointer data) +{ + g_autoptr(GKeyFile) keyfile = data; + g_autofree char *change = NULL; + + change = g_key_file_get_string (keyfile, "backend", "change", NULL); + + g_debug ("change session state: %s\n", change); + + if (change && g_str_has_prefix (change, "query-end")) + { + wait_for_query_end_response (NULL); + set_session_state (QUERY_END); + maybe_send_quit_response (); + } + + return G_SOURCE_REMOVE; +} + +static gboolean +handle_create_monitor (XdpDbusImplInhibit *object, + GDBusMethodInvocation *invocation, + const char *arg_handle, + const char *arg_session_handle, + const char *arg_app_id, + const char *arg_window) +{ + g_autoptr(GError) error = NULL; + int response; + Session *session; + const char *dir; + g_autofree char *path = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + int delay; + + g_debug ("Handling CreateMonitor"); + + session_state = RUNNING; + screensaver_active = FALSE; + + dir = g_getenv ("XDG_DATA_HOME"); + path = g_build_filename (dir, "inhibit", NULL); + keyfile = g_key_file_new (); + g_key_file_load_from_file (keyfile, path, 0, &error); + g_assert_no_error (error); + + session = (Session *)inhibit_session_new (arg_app_id, arg_session_handle); + + if (!session_export (session, g_dbus_method_invocation_get_connection (invocation), &error)) + { + g_clear_object (&session); + g_warning ("Failed to create inhibit session: %s", error->message); + response = 2; + goto out; + } + + response = 0; + + delay = g_key_file_get_integer (keyfile, "backend", "delay", NULL); + + g_debug ("delay %d", delay); + + if (delay != 0) + g_timeout_add (delay, change_session_state, g_key_file_ref (keyfile)); + +out: + xdp_dbus_impl_inhibit_complete_create_monitor (object, invocation, response); + if (session) + emit_state_changed (session); + + return TRUE; +} + +static gboolean +handle_query_end_response (XdpDbusImplInhibit *object, + GDBusMethodInvocation *invocation, + const char *arg_session_handle) +{ + InhibitSession *session = (InhibitSession *)lookup_session (arg_session_handle); + + g_debug ("Handle QueryEndResponse for session %s", arg_session_handle); + + if (session) + { + session->pending_query_end_response = FALSE; + maybe_send_quit_response (); + } + + xdp_dbus_impl_inhibit_complete_query_end_response (object, invocation); + + return TRUE; +} + +void +inhibit_init (GDBusConnection *connection, + const char *object_path) +{ + g_autoptr(GError) error = NULL; + + inhibit = G_DBUS_INTERFACE_SKELETON (xdp_dbus_impl_inhibit_skeleton_new ()); + + g_signal_connect (inhibit, "handle-inhibit", G_CALLBACK (handle_inhibit), NULL); + g_signal_connect (inhibit, "handle-create-monitor", G_CALLBACK (handle_create_monitor), NULL); + g_signal_connect (inhibit, "handle-query-end-response", G_CALLBACK (handle_query_end_response), NULL); + + if (!g_dbus_interface_skeleton_export (inhibit, connection, object_path, &error)) + { + g_error ("Failed to export %s skeleton: %s\n", + g_dbus_interface_skeleton_get_info (inhibit)->name, + error->message); + exit (1); + } + + g_debug ("providing %s at %s", g_dbus_interface_skeleton_get_info (inhibit)->name, object_path); +} diff --git a/tests/backend/inhibit.h b/tests/backend/inhibit.h new file mode 100644 index 0000000..9a42bd0 --- /dev/null +++ b/tests/backend/inhibit.h @@ -0,0 +1,3 @@ +#pragma once + +void inhibit_init (GDBusConnection *connection, const char *object_path); diff --git a/tests/backend/lockdown.c b/tests/backend/lockdown.c new file mode 100644 index 0000000..f5b65ec --- /dev/null +++ b/tests/backend/lockdown.c @@ -0,0 +1,43 @@ +#define _GNU_SOURCE 1 + +#include "config.h" +#include + +#include + +#include "xdp-impl-dbus.h" + +#include "lockdown.h" + +static void +property_changed (GObject *object, + GParamSpec *pspec, + gpointer data) +{ + gboolean value; + + g_object_get (object, pspec->name, &value, NULL); + g_debug ("lockdown change: %s: %d", pspec->name, value); +} + +void +lockdown_init (GDBusConnection *bus, + const char *object_path) +{ + GDBusInterfaceSkeleton *helper; + g_autoptr(GError) error = NULL; + + helper = G_DBUS_INTERFACE_SKELETON (xdp_dbus_impl_lockdown_skeleton_new ()); + + if (!g_dbus_interface_skeleton_export (helper, bus, object_path, &error)) + { + g_error ("Failed to export %s skeleton: %s\n", + g_dbus_interface_skeleton_get_info (helper)->name, + error->message); + exit (1); + } + g_signal_connect (helper, "notify", G_CALLBACK (property_changed), NULL); + + g_debug ("providing %s at %s", g_dbus_interface_skeleton_get_info (helper)->name, object_path); +} + diff --git a/tests/backend/lockdown.h b/tests/backend/lockdown.h new file mode 100644 index 0000000..b39262c --- /dev/null +++ b/tests/backend/lockdown.h @@ -0,0 +1,5 @@ +#pragma once + +void lockdown_init (GDBusConnection *connection, const char *object_path); + +void lockdown_update (void); diff --git a/tests/backend/notification.c b/tests/backend/notification.c new file mode 100644 index 0000000..0fcb856 --- /dev/null +++ b/tests/backend/notification.c @@ -0,0 +1,121 @@ +#include +#include +#include + +#include + +#include "xdp-impl-dbus.h" + +#include "notification.h" + +typedef struct { + XdpDbusImplNotification *impl; + char *app_id; + char *id; + char *action; +} ActionData; + +static gboolean +invoke_action (gpointer data) +{ + ActionData *adata = data; + GVariantBuilder builder; + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("av")); + + g_print ("emitting ActionInvoked\n"); + xdp_dbus_impl_notification_emit_action_invoked (adata->impl, + adata->app_id, + adata->id, + adata->action, + g_variant_builder_end (&builder)); + + g_free (adata->app_id); + g_free (adata->id); + g_free (adata->action); + g_free (adata); + + return G_SOURCE_REMOVE; +} + +static gboolean +handle_add_notification (XdpDbusImplNotification *object, + GDBusMethodInvocation *invocation, + const gchar *arg_app_id, + const gchar *arg_id, + GVariant *arg_notification) +{ + const char *dir; + g_autofree char *path = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autofree char *notification_s = NULL; + g_autoptr(GVariant) notification = NULL; + g_autoptr(GError) error = NULL; + int delay; + + dir = g_getenv ("XDG_DATA_HOME"); + path = g_build_filename (dir, "notification", NULL); + keyfile = g_key_file_new (); + g_key_file_load_from_file (keyfile, path, 0, &error); + g_assert_no_error (error); + + notification_s = g_key_file_get_string (keyfile, "notification", "data", NULL); + notification = g_variant_parse (G_VARIANT_TYPE_VARDICT, notification_s, NULL, NULL, &error); + g_assert_no_error (error); + g_assert_true (g_variant_equal (notification, arg_notification)); + + if (g_key_file_get_boolean (keyfile, "backend", "expect-no-call", NULL)) + g_assert_not_reached (); + + delay = g_key_file_get_integer (keyfile, "backend", "delay", NULL); + if (delay != 0) + { + ActionData *data; + data = g_new (ActionData, 1); + data->impl = object; + data->app_id = g_strdup (arg_app_id); + data->id = g_strdup (arg_id); + data->action = g_key_file_get_string (keyfile, "notification", "action", NULL); + + g_timeout_add (delay, invoke_action, data); + } + + xdp_dbus_impl_notification_complete_add_notification (object, invocation); + + return TRUE; +} + +static gboolean +handle_remove_notification (XdpDbusImplNotification *object, + GDBusMethodInvocation *invocation, + const gchar *arg_app_id, + const gchar *arg_id) +{ + xdp_dbus_impl_notification_complete_remove_notification (object, invocation); + + return TRUE; +} + +void +notification_init (GDBusConnection *bus, + const char *object_path) +{ + g_autoptr(GError) error = NULL; + GDBusInterfaceSkeleton *helper; + + helper = G_DBUS_INTERFACE_SKELETON (xdp_dbus_impl_notification_skeleton_new ()); + + g_signal_connect (helper, "handle-add-notification", G_CALLBACK (handle_add_notification), NULL); + g_signal_connect (helper, "handle-remove-notification", G_CALLBACK (handle_remove_notification), NULL); + + if (!g_dbus_interface_skeleton_export (helper, bus, object_path, &error)) + { + g_error ("Failed to export %s skeleton: %s\n", + g_dbus_interface_skeleton_get_info (helper)->name, + error->message); + exit (1); + } + + g_debug ("providing %s at %s", g_dbus_interface_skeleton_get_info (helper)->name, object_path); +} + diff --git a/tests/backend/notification.h b/tests/backend/notification.h new file mode 100644 index 0000000..629b587 --- /dev/null +++ b/tests/backend/notification.h @@ -0,0 +1,3 @@ +#pragma once + +void notification_init (GDBusConnection *connection, const char *object_path); diff --git a/tests/backend/print.c b/tests/backend/print.c new file mode 100644 index 0000000..3f18e40 --- /dev/null +++ b/tests/backend/print.c @@ -0,0 +1,260 @@ +#include +#include +#include +#include + +#include + +#include "xdp-impl-dbus.h" + +#include "request.h" +#include "print.h" + +typedef struct { + XdpDbusImplPrint *impl; + GDBusMethodInvocation *invocation; + Request *request; + GKeyFile *keyfile; + char *app_id; + guint timeout; + char *title; + GVariant *settings; + GVariant *page_setup; + GVariant *options; +} PrintHandle; + +static void +print_handle_free (PrintHandle *handle) +{ + g_object_unref (handle->impl); + g_object_unref (handle->request); + g_key_file_unref (handle->keyfile); + g_free (handle->app_id); + if (handle->timeout) + g_source_remove (handle->timeout); + g_free (handle->title); + if (handle->settings) + g_variant_unref (handle->settings); + if (handle->page_setup) + g_variant_unref (handle->page_setup); + if (handle->options) + g_variant_unref (handle->options); + + g_free (handle); +} + +static gboolean +send_response (gpointer data) +{ + PrintHandle *handle = data; + GVariantBuilder opt_builder; + int response; + int token; + GVariantBuilder settings; + GVariantBuilder page_setup; + + if (g_key_file_get_boolean (handle->keyfile, "backend", "expect-close", NULL)) + g_assert_not_reached (); + + response = g_key_file_get_integer (handle->keyfile, "backend", "response", NULL); + + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + + if (handle->request->exported) + request_unexport (handle->request); + + if (strcmp (g_dbus_method_invocation_get_method_name (handle->invocation), "PreparePrint") == 0) + { + token = g_key_file_get_integer (handle->keyfile, "result", "token", NULL); + g_variant_builder_init (&settings, G_VARIANT_TYPE_VARDICT); + g_variant_builder_init (&page_setup, G_VARIANT_TYPE_VARDICT); + + g_variant_builder_add (&opt_builder, "{sv}", "token", g_variant_new_uint32 (token)); + g_variant_builder_add (&opt_builder, "{sv}", "settings", g_variant_builder_end (&settings)); + g_variant_builder_add (&opt_builder, "{sv}", "page-setup", g_variant_builder_end (&page_setup)); + } + + g_debug ("send response %d", response); + + if (strcmp (g_dbus_method_invocation_get_method_name (handle->invocation), "Print") == 0) + xdp_dbus_impl_print_complete_print (handle->impl, + handle->invocation, + NULL, + response, + g_variant_builder_end (&opt_builder)); + else + xdp_dbus_impl_print_complete_prepare_print (handle->impl, + handle->invocation, + response, + g_variant_builder_end (&opt_builder)); + + handle->timeout = 0; + + print_handle_free (handle); + + return G_SOURCE_REMOVE; +} + +static gboolean +handle_close (XdpDbusImplRequest *object, + GDBusMethodInvocation *invocation, + PrintHandle *handle) +{ + GVariantBuilder opt_builder; + + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + g_debug ("send response 2"); + if (strcmp (g_dbus_method_invocation_get_method_name (handle->invocation), "Print") == 0) + xdp_dbus_impl_print_complete_print (handle->impl, + handle->invocation, + NULL, + 2, + g_variant_builder_end (&opt_builder)); + else + xdp_dbus_impl_print_complete_prepare_print (handle->impl, + handle->invocation, + 2, + g_variant_builder_end (&opt_builder)); + print_handle_free (handle); + + return FALSE; +} + + +static gboolean +handle_print (XdpDbusImplPrint *object, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + const char *arg_handle, + const char *arg_app_id, + const char *arg_parent_window, + const char *arg_title, + GVariant *arg_fd, + GVariant *arg_options) +{ + const char *sender; + const char *dir; + g_autofree char *path = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + int delay; + PrintHandle *handle; + g_autoptr(Request) request = NULL; + + g_debug ("Handling Print"); + + sender = g_dbus_method_invocation_get_sender (invocation); + + dir = g_getenv ("XDG_DATA_HOME"); + path = g_build_filename (dir, "print", NULL); + keyfile = g_key_file_new (); + g_key_file_load_from_file (keyfile, path, 0, &error); + g_assert_no_error (error); + + request = request_new (sender, arg_app_id, arg_handle); + + handle = g_new0 (PrintHandle, 1); + handle->impl = g_object_ref (object); + handle->invocation = invocation; + handle->request = g_object_ref (request); + handle->keyfile = g_key_file_ref (keyfile); + handle->app_id = g_strdup (arg_app_id); + + g_signal_connect (request, "handle-close", G_CALLBACK (handle_close), handle); + + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + if (g_key_file_has_key (keyfile, "backend", "delay", NULL)) + delay = g_key_file_get_integer (keyfile, "backend", "delay", NULL); + else + delay = 200; + + g_debug ("delay %d", delay); + + if (delay == 0) + send_response (handle); + else + handle->timeout = g_timeout_add (delay, send_response, handle); + + return TRUE; +} + +static gboolean +handle_prepare_print (XdpDbusImplPrint *object, + GDBusMethodInvocation *invocation, + const char *arg_handle, + const char *arg_app_id, + const char *arg_parent_window, + const char *arg_title, + GVariant *arg_settings, + GVariant *arg_page_setup, + GVariant *arg_options) +{ + const char *sender; + const char *dir; + g_autofree char *path = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + int delay; + PrintHandle *handle; + g_autoptr(Request) request = NULL; + + g_debug ("Handling Print"); + + sender = g_dbus_method_invocation_get_sender (invocation); + + dir = g_getenv ("XDG_DATA_HOME"); + path = g_build_filename (dir, "print", NULL); + keyfile = g_key_file_new (); + g_key_file_load_from_file (keyfile, path, 0, &error); + g_assert_no_error (error); + + request = request_new (sender, arg_app_id, arg_handle); + + handle = g_new0 (PrintHandle, 1); + handle->impl = g_object_ref (object); + handle->invocation = invocation; + handle->request = g_object_ref (request); + handle->keyfile = g_key_file_ref (keyfile); + handle->app_id = g_strdup (arg_app_id); + + g_signal_connect (request, "handle-close", G_CALLBACK (handle_close), handle); + + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + if (g_key_file_has_key (keyfile, "backend", "delay", NULL)) + delay = g_key_file_get_integer (keyfile, "backend", "delay", NULL); + else + delay = 200; + + g_debug ("delay %d", delay); + + if (delay == 0) + send_response (handle); + else + handle->timeout = g_timeout_add (delay, send_response, handle); + + return TRUE; +} +void +print_init (GDBusConnection *connection, + const char *object_path) +{ + g_autoptr(GError) error = NULL; + GDBusInterfaceSkeleton *helper; + + helper = G_DBUS_INTERFACE_SKELETON (xdp_dbus_impl_print_skeleton_new ()); + + g_signal_connect (helper, "handle-print", G_CALLBACK (handle_print), NULL); + g_signal_connect (helper, "handle-prepare-print", G_CALLBACK (handle_prepare_print), NULL); + + if (!g_dbus_interface_skeleton_export (helper, connection, object_path, &error)) + { + g_error ("Failed to export %s skeleton: %s\n", + g_dbus_interface_skeleton_get_info (helper)->name, + error->message); + exit (1); + } + + g_debug ("providing %s at %s", g_dbus_interface_skeleton_get_info (helper)->name, object_path); +} diff --git a/tests/backend/print.h b/tests/backend/print.h new file mode 100644 index 0000000..37c63e1 --- /dev/null +++ b/tests/backend/print.h @@ -0,0 +1,3 @@ +#pragma once + +void print_init (GDBusConnection *connection, const char *object_path); diff --git a/tests/backend/request.c b/tests/backend/request.c new file mode 100644 index 0000000..f16d63e --- /dev/null +++ b/tests/backend/request.c @@ -0,0 +1,119 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Alexander Larsson + * Matthias Clasen + */ + +#include "request.h" + +#include + +static void request_skeleton_iface_init (XdpDbusImplRequestIface *iface); + +G_DEFINE_TYPE_WITH_CODE (Request, request, XDP_DBUS_IMPL_TYPE_REQUEST_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_IMPL_TYPE_REQUEST, + request_skeleton_iface_init)) + +static gboolean +handle_close (XdpDbusImplRequest *object, + GDBusMethodInvocation *invocation) +{ + Request *request = (Request *)object; + + if (request->exported) + request_unexport (request); + + xdp_dbus_impl_request_complete_close (XDP_DBUS_IMPL_REQUEST (request), + invocation); + + return TRUE; +} + +static void +request_skeleton_iface_init (XdpDbusImplRequestIface *iface) +{ + iface->handle_close = handle_close; +} + +static void +request_init (Request *request) +{ +} + +static void +request_finalize (GObject *object) +{ + Request *request = (Request *)object; + + g_free (request->sender); + g_free (request->app_id); + g_free (request->id); + + G_OBJECT_CLASS (request_parent_class)->finalize (object); +} + +static void +request_class_init (RequestClass *klass) +{ + GObjectClass *gobject_class; + + gobject_class = G_OBJECT_CLASS (klass); + gobject_class->finalize = request_finalize; +} + +Request * +request_new (const char *sender, + const char *app_id, + const char *id) +{ + Request *request; + + request = g_object_new (request_get_type (), NULL); + request->sender = g_strdup (sender); + request->app_id = g_strdup (app_id); + request->id = g_strdup (id); + + return request; +} + +void +request_export (Request *request, + GDBusConnection *connection) +{ + g_autoptr(GError) error = NULL; + + if (!g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (request), + connection, + request->id, + &error)) + { + g_warning ("error exporting request: %s\n", error->message); + g_clear_error (&error); + } + + g_object_ref (request); + request->exported = TRUE; +} + +void +request_unexport (Request *request) +{ + request->exported = FALSE; + g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (request)); + g_object_unref (request); +} diff --git a/tests/backend/request.h b/tests/backend/request.h new file mode 100644 index 0000000..9a150eb --- /dev/null +++ b/tests/backend/request.h @@ -0,0 +1,54 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Alexander Larsson + * Matthias Clasen + */ + +#pragma once + +#include "src/xdp-impl-dbus.h" + +typedef struct _Request Request; +typedef struct _RequestClass RequestClass; + +struct _Request +{ + XdpDbusImplRequestSkeleton parent_instance; + + gboolean exported; + char *sender; + char *app_id; + char *id; +}; + +struct _RequestClass +{ + XdpDbusImplRequestSkeletonClass parent_class; +}; + +GType request_get_type (void) G_GNUC_CONST; + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (Request, g_object_unref) + +Request *request_new (const char *sender, + const char *app_id, + const char *id); + +void request_export (Request *request, + GDBusConnection *connection); +void request_unexport (Request *request); diff --git a/tests/backend/screenshot.c b/tests/backend/screenshot.c new file mode 100644 index 0000000..3cf06c5 --- /dev/null +++ b/tests/backend/screenshot.c @@ -0,0 +1,198 @@ +#include +#include +#include +#include + +#include + +#include "xdp-impl-dbus.h" + +#include "request.h" +#include "screenshot.h" + +typedef struct { + XdpDbusImplScreenshot *impl; + GDBusMethodInvocation *invocation; + Request *request; + GKeyFile *keyfile; + char *app_id; + guint timeout; + gboolean is_screenshot; +} ScreenshotHandle; + +static void +screenshot_handle_free (ScreenshotHandle *handle) +{ + g_object_unref (handle->impl); + g_object_unref (handle->request); + g_key_file_unref (handle->keyfile); + g_free (handle->app_id); + if (handle->timeout) + g_source_remove (handle->timeout); + + g_free (handle); +} + +static gboolean +send_response (gpointer data) +{ + ScreenshotHandle *handle = data; + GVariantBuilder opt_builder; + int response; + g_autofree char *uri = NULL; + double red, green, blue; + + if (g_key_file_get_boolean (handle->keyfile, "backend", "expect-close", NULL)) + g_assert_not_reached (); + + response = g_key_file_get_integer (handle->keyfile, "backend", "response", NULL); + + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + + if (response == 0) + { + if (handle->is_screenshot) + { + uri = g_key_file_get_string (handle->keyfile, "result", "uri", NULL); + g_variant_builder_add (&opt_builder, "{sv}", "uri", g_variant_new_string (uri)); + } + else + { + red = g_key_file_get_double (handle->keyfile,"result", "red", NULL); + green = g_key_file_get_double (handle->keyfile,"result", "green", NULL); + blue = g_key_file_get_double (handle->keyfile,"result", "blue", NULL); + g_variant_builder_add (&opt_builder, "{sv}", "color", g_variant_new ("(ddd)", red, green, blue)); + } + } + + if (handle->request->exported) + request_unexport (handle->request); + + g_debug ("send response %d", response); + + if (handle->is_screenshot) + xdp_dbus_impl_screenshot_complete_screenshot (handle->impl, + handle->invocation, + response, + g_variant_builder_end (&opt_builder)); + else + xdp_dbus_impl_screenshot_complete_pick_color (handle->impl, + handle->invocation, + response, + g_variant_builder_end (&opt_builder)); + + handle->timeout = 0; + + screenshot_handle_free (handle); + + return G_SOURCE_REMOVE; +} + +static gboolean +handle_close (XdpDbusImplRequest *object, + GDBusMethodInvocation *invocation, + ScreenshotHandle *handle) +{ + GVariantBuilder opt_builder; + + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + g_debug ("handling Close"); + if (handle->is_screenshot) + xdp_dbus_impl_screenshot_complete_screenshot (handle->impl, + handle->invocation, + 2, + g_variant_builder_end (&opt_builder)); + else + xdp_dbus_impl_screenshot_complete_pick_color (handle->impl, + handle->invocation, + 2, + g_variant_builder_end (&opt_builder)); + + screenshot_handle_free (handle); + + return FALSE; +} + + +static gboolean +handle_screenshot (XdpDbusImplScreenshot *object, + GDBusMethodInvocation *invocation, + const char *arg_handle, + const char *arg_app_id, + const char *arg_parent_window, + GVariant *arg_options) +{ + const char *sender; + const char *dir; + g_autofree char *path = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + int delay; + ScreenshotHandle *handle; + g_autoptr(Request) request = NULL; + + g_debug ("Handling %s", g_dbus_method_invocation_get_method_name (invocation)); + + sender = g_dbus_method_invocation_get_sender (invocation); + + dir = g_getenv ("XDG_DATA_HOME"); + path = g_build_filename (dir, "screenshot", NULL); + keyfile = g_key_file_new (); + g_key_file_load_from_file (keyfile, path, 0, &error); + g_assert_no_error (error); + + request = request_new (sender, arg_app_id, arg_handle); + + handle = g_new0 (ScreenshotHandle, 1); + handle->impl = g_object_ref (object); + handle->invocation = invocation; + handle->request = g_object_ref (request); + handle->keyfile = g_key_file_ref (keyfile); + handle->app_id = g_strdup (arg_app_id); + + if (strcmp (g_dbus_method_invocation_get_method_name (invocation), + "Screenshot") == 0) + handle->is_screenshot = TRUE; + + g_signal_connect (request, "handle-close", G_CALLBACK (handle_close), handle); + + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + if (g_key_file_has_key (keyfile, "backend", "delay", NULL)) + delay = g_key_file_get_integer (keyfile, "backend", "delay", NULL); + else + delay = 200; + + g_debug ("delay %d", delay); + + if (delay == 0) + send_response (handle); + else + handle->timeout = g_timeout_add (delay, send_response, handle); + + return TRUE; +} + +void +screenshot_init (GDBusConnection *connection, + const char *object_path) +{ + g_autoptr(GError) error = NULL; + GDBusInterfaceSkeleton *helper; + + helper = G_DBUS_INTERFACE_SKELETON (xdp_dbus_impl_screenshot_skeleton_new ()); + + xdp_dbus_impl_screenshot_set_version (XDP_DBUS_IMPL_SCREENSHOT (helper), 2); + g_signal_connect (helper, "handle-screenshot", G_CALLBACK (handle_screenshot), NULL); + g_signal_connect (helper, "handle-pick-color", G_CALLBACK (handle_screenshot), NULL); + + if (!g_dbus_interface_skeleton_export (helper, connection, object_path, &error)) + { + g_error ("Failed to export %s skeleton: %s\n", + g_dbus_interface_skeleton_get_info (helper)->name, + error->message); + exit (1); + } + + g_debug ("providing %s at %s", g_dbus_interface_skeleton_get_info (helper)->name, object_path); +} diff --git a/tests/backend/screenshot.h b/tests/backend/screenshot.h new file mode 100644 index 0000000..61ae39b --- /dev/null +++ b/tests/backend/screenshot.h @@ -0,0 +1,3 @@ +#pragma once + +void screenshot_init (GDBusConnection *connection, const char *object_path); diff --git a/tests/backend/session.c b/tests/backend/session.c new file mode 100644 index 0000000..ff0a3af --- /dev/null +++ b/tests/backend/session.c @@ -0,0 +1,195 @@ + +/* + * Copyright © 2017 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + */ + +#include "session.h" + +enum +{ + PROP_0, + + PROP_ID, + + PROP_LAST +}; + +static GParamSpec *obj_props[PROP_LAST]; + +static GHashTable *sessions; + +static void session_skeleton_iface_init (XdpDbusImplSessionIface *iface); + +G_DEFINE_TYPE_WITH_CODE (Session, session, XDP_DBUS_IMPL_TYPE_SESSION_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_IMPL_TYPE_SESSION, + session_skeleton_iface_init)) + +#define SESSION_GET_CLASS(o) \ + (G_TYPE_INSTANCE_GET_CLASS ((o), session_get_type (), SessionClass)) + +Session * +lookup_session (const char *id) +{ + return g_hash_table_lookup (sessions, id); +} + +gboolean +session_export (Session *session, + GDBusConnection *connection, + GError **error) +{ + if (!g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (session), + connection, + session->id, + error)) + return FALSE; + + g_object_ref (session); + session->exported = TRUE; + + return TRUE; +} + +void +session_unexport (Session *session) +{ + session->exported = FALSE; + g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (session)); + g_object_unref (session); +} + +void +session_close (Session *session) +{ + if (session->exported) + session_unexport (session); + + session->closed = TRUE; + + SESSION_GET_CLASS (session)->close (session); + + g_object_unref (session); +} + +static gboolean +handle_close (XdpDbusImplSession *object, + GDBusMethodInvocation *invocation) +{ + Session *session = (Session *)object; + + if (!session->closed) + session_close (session); + + xdp_dbus_impl_session_complete_close (object, invocation); + + return TRUE; +} + +static void +session_skeleton_iface_init (XdpDbusImplSessionIface *iface) +{ + iface->handle_close = handle_close; +} + +static void +session_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + Session *session = (Session *)object; + + switch (prop_id) + { + case PROP_ID: + session->id = g_strdup (g_value_get_string (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +session_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + Session *session = (Session *)object; + + switch (prop_id) + { + case PROP_ID: + g_value_set_string (value, session->id); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +session_finalize (GObject *object) +{ + Session *session = (Session *)object; + + g_hash_table_remove (sessions, session->id); + + g_free (session->id); + + G_OBJECT_CLASS (session_parent_class)->finalize (object); +} + +static void +session_constructed (GObject *object) +{ + Session *session = (Session *)object; + + g_hash_table_insert (sessions, g_strdup (session->id), session); + + G_OBJECT_CLASS (session_parent_class)->constructed (object); +} + +static void +session_init (Session *session) +{ +} + +static void +session_class_init (SessionClass *klass) +{ + GObjectClass *gobject_class; + + gobject_class = G_OBJECT_CLASS (klass); + gobject_class->constructed = session_constructed; + gobject_class->finalize = session_finalize; + gobject_class->set_property = session_set_property; + gobject_class->get_property = session_get_property; + + obj_props[PROP_ID] = + g_param_spec_string ("id", "id", "ID", + NULL, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (gobject_class, PROP_LAST, obj_props); + + sessions = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, NULL); +} diff --git a/tests/backend/session.h b/tests/backend/session.h new file mode 100644 index 0000000..874cc96 --- /dev/null +++ b/tests/backend/session.h @@ -0,0 +1,56 @@ +/* + * Copyright © 2017 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + */ + +#pragma once + +#include "src/xdp-impl-dbus.h" + +typedef struct _Session Session; +typedef struct _SessionClass SessionClass; + +struct _Session +{ + XdpDbusImplSessionSkeleton parent; + + gboolean exported; + gboolean closed; + char *id; +}; + +struct _SessionClass +{ + XdpDbusImplSessionSkeletonClass parent_class; + + void (*close) (Session *session); +}; + +GType session_get_type (void); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (Session, g_object_unref) + +Session *lookup_session (const char *id); + +Session *session_new (const char *id); + +void session_close (Session *session); + +gboolean session_export (Session *session, + GDBusConnection *connection, + GError **error); + +void session_unexport (Session *session); diff --git a/tests/backend/settings.c b/tests/backend/settings.c new file mode 100644 index 0000000..bf4d23c --- /dev/null +++ b/tests/backend/settings.c @@ -0,0 +1,31 @@ +#include +#include +#include +#include + +#include + +#include "xdp-impl-dbus.h" + +#include "request.h" +#include "settings.h" + +void +settings_init (GDBusConnection *connection, + const char *object_path) +{ + g_autoptr(GError) error = NULL; + GDBusInterfaceSkeleton *helper; + + helper = G_DBUS_INTERFACE_SKELETON (xdp_dbus_impl_settings_skeleton_new ()); + + if (!g_dbus_interface_skeleton_export (helper, connection, object_path, &error)) + { + g_error ("Failed to export %s skeleton: %s\n", + g_dbus_interface_skeleton_get_info (helper)->name, + error->message); + exit (1); + } + + g_debug ("providing %s at %s", g_dbus_interface_skeleton_get_info (helper)->name, object_path); +} diff --git a/tests/backend/settings.h b/tests/backend/settings.h new file mode 100644 index 0000000..4b4c3dc --- /dev/null +++ b/tests/backend/settings.h @@ -0,0 +1,3 @@ +#pragma once + +void settings_init (GDBusConnection *connection, const char *object_path); diff --git a/tests/backend/test-backends.c b/tests/backend/test-backends.c new file mode 100644 index 0000000..bf6261f --- /dev/null +++ b/tests/backend/test-backends.c @@ -0,0 +1,159 @@ +#include +#include + +#include + +#include "xdp-impl-dbus.h" + +#include "access.h" +#include "account.h" +#include "appchooser.h" +#include "background.h" +#include "email.h" +#include "filechooser.h" +#include "inhibit.h" +#include "lockdown.h" +#include "notification.h" +#include "print.h" +#include "screenshot.h" +#include "settings.h" +#include "wallpaper.h" + +#include "src/glib-backports.h" + +#define BACKEND_OBJECT_PATH "/org/freedesktop/portal/desktop" + +static GMainLoop *loop; + +static void +on_bus_acquired (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + access_init (connection, BACKEND_OBJECT_PATH); + account_init (connection, BACKEND_OBJECT_PATH); + appchooser_init (connection, BACKEND_OBJECT_PATH); + background_init (connection, BACKEND_OBJECT_PATH); + email_init (connection, BACKEND_OBJECT_PATH); + file_chooser_init (connection, BACKEND_OBJECT_PATH); + inhibit_init (connection, BACKEND_OBJECT_PATH); + lockdown_init (connection, BACKEND_OBJECT_PATH); + notification_init (connection, BACKEND_OBJECT_PATH); + print_init (connection, BACKEND_OBJECT_PATH); + screenshot_init (connection, BACKEND_OBJECT_PATH); + settings_init (connection, BACKEND_OBJECT_PATH); + wallpaper_init (connection, BACKEND_OBJECT_PATH); +} + +static void +on_name_acquired (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + g_debug ("%s acquired", name); +} + +static void +on_name_lost (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + g_debug ("%s lost", name); + g_main_loop_quit (loop); +} + +static gboolean opt_verbose; +static gboolean opt_replace; +static char *opt_backend_name; + +static GOptionEntry entries[] = { + { "backend-name", 0, 0, G_OPTION_ARG_STRING, &opt_backend_name, "The name of the backend on the bus", NULL }, + { "verbose", 'v', 0, G_OPTION_ARG_NONE, &opt_verbose, "Print debug information during command processing", NULL }, + { "replace", 'r', 0, G_OPTION_ARG_NONE, &opt_replace, "Replace a running instance", NULL }, + { NULL } +}; + +static void +message_handler (const char *log_domain, + GLogLevelFlags log_level, + const char *message, + gpointer user_data) +{ + if (log_level & G_LOG_LEVEL_DEBUG) + printf ("TST: %s\n", message); + else + printf ("%s: %s\n", g_get_prgname (), message); +} + +static void +printerr_handler (const gchar *string) +{ + int is_tty = isatty (1); + const char *prefix = ""; + const char *suffix = ""; + if (is_tty) + { + prefix = "\x1b[31m\x1b[1m"; /* red, bold */ + suffix = "\x1b[22m\x1b[0m"; /* bold off, color reset */ + } + fprintf (stderr, "%serror: %s%s\n", prefix, suffix, string); +} + +int +main (int argc, char *argv[]) +{ + guint owner_id; + g_autoptr(GError) error = NULL; + g_autoptr(GDBusConnection) session_bus = NULL; + g_autoptr(GOptionContext) context = NULL; + + g_log_writer_default_set_use_stderr (TRUE); + + g_setenv ("GIO_USE_VFS", "local", TRUE); + + g_set_prgname (argv[0]); + + context = g_option_context_new ("- portal test backends"); + g_option_context_add_main_entries (context, entries, NULL); + if (!g_option_context_parse (context, &argc, &argv, &error)) + { + g_printerr ("%s: %s", g_get_application_name (), error->message); + g_printerr ("\n"); + return 1; + } + + if (opt_backend_name == NULL) + g_error ("You must specify the name of the backend to own on the bus"); + + g_set_printerr_handler (printerr_handler); + if (opt_verbose) + g_log_set_handler (G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, message_handler, NULL); + + loop = g_main_loop_new (NULL, FALSE); + + session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); + if (session_bus == NULL) + { + g_printerr ("No session bus: %s", error->message); + return 2; + } + + g_debug ("Testing backends for '%s'", opt_backend_name); + owner_id = g_bus_own_name (G_BUS_TYPE_SESSION, + opt_backend_name, + G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | (opt_replace ? G_BUS_NAME_OWNER_FLAGS_REPLACE : 0), + on_bus_acquired, + on_name_acquired, + on_name_lost, + NULL, + NULL); + + g_main_loop_run (loop); + + g_bus_unown_name (owner_id); + g_main_loop_unref (loop); + + g_debug ("%s exiting.", g_get_prgname ()); + + return 0; +} diff --git a/tests/backend/wallpaper.c b/tests/backend/wallpaper.c new file mode 100644 index 0000000..dfbf07f --- /dev/null +++ b/tests/backend/wallpaper.c @@ -0,0 +1,170 @@ +#include +#include +#include + +#include + +#include "xdp-impl-dbus.h" + +#include "request.h" +#include "wallpaper.h" + +typedef struct { + XdpDbusImplWallpaper *impl; + GDBusMethodInvocation *invocation; + Request *request; + GKeyFile *keyfile; + char *app_id; + guint timeout; + char *uri; + GVariant *options; +} WallpaperHandle; + +static void +wallpaper_handle_free (WallpaperHandle *handle) +{ + g_object_unref (handle->impl); + g_object_unref (handle->request); + g_key_file_unref (handle->keyfile); + g_free (handle->app_id); + if (handle->timeout) + g_source_remove (handle->timeout); + g_free (handle->uri); + g_variant_unref (handle->options); + + g_free (handle); +} + +static gboolean +send_response (gpointer data) +{ + WallpaperHandle *handle = data; + int response; + g_autofree char *s1 = NULL; + const char *s; + gboolean b1, b; + + if (g_key_file_get_boolean (handle->keyfile, "backend", "expect-close", NULL)) + g_assert_not_reached (); + + response = g_key_file_get_integer (handle->keyfile, "backend", "response", NULL); + + if (handle->request->exported) + request_unexport (handle->request); + + s1 = g_key_file_get_string (handle->keyfile, "wallpaper", "target", NULL); + g_variant_lookup (handle->options, "set-on", "&s", &s); + g_assert_cmpstr (s1, ==, s); + + b1 = g_key_file_get_boolean (handle->keyfile, "wallpaper", "preview", NULL); + g_variant_lookup (handle->options, "show-preview", "b", &b); + g_assert_cmpint (b1, ==, b); + + g_debug ("send response %d", response); + + xdp_dbus_impl_wallpaper_complete_set_wallpaper_uri (handle->impl, + handle->invocation, + response); + + handle->timeout = 0; + + wallpaper_handle_free (handle); + + return G_SOURCE_REMOVE; +} + +static gboolean +handle_close (XdpDbusImplRequest *object, + GDBusMethodInvocation *invocation, + WallpaperHandle *handle) +{ + + g_debug ("send response 2"); + xdp_dbus_impl_wallpaper_complete_set_wallpaper_uri (handle->impl, + handle->invocation, + 2); + wallpaper_handle_free (handle); + + return FALSE; +} + + +static gboolean +handle_set_wallpaper_uri (XdpDbusImplWallpaper *object, + GDBusMethodInvocation *invocation, + const char *arg_handle, + const char *arg_app_id, + const char *arg_parent_window, + const char *arg_uri, + GVariant *arg_options) +{ + const char *sender; + const char *dir; + g_autofree char *path = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + int delay; + WallpaperHandle *handle; + g_autoptr(Request) request = NULL; + + g_debug ("Handling SetWallpaperURI"); + + sender = g_dbus_method_invocation_get_sender (invocation); + + dir = g_getenv ("XDG_DATA_HOME"); + path = g_build_filename (dir, "wallpaper", NULL); + keyfile = g_key_file_new (); + g_key_file_load_from_file (keyfile, path, 0, &error); + g_assert_no_error (error); + + request = request_new (sender, arg_app_id, arg_handle); + + handle = g_new0 (WallpaperHandle, 1); + handle->impl = g_object_ref (object); + handle->invocation = invocation; + handle->request = g_object_ref (request); + handle->keyfile = g_key_file_ref (keyfile); + handle->app_id = g_strdup (arg_app_id); + handle->uri = g_strdup (arg_uri); + handle->options = g_variant_ref (arg_options); + + g_signal_connect (request, "handle-close", G_CALLBACK (handle_close), handle); + + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + if (g_key_file_has_key (keyfile, "backend", "delay", NULL)) + delay = g_key_file_get_integer (keyfile, "backend", "delay", NULL); + else + delay = 200; + + g_debug ("delay %d", delay); + + if (delay == 0) + send_response (handle); + else + handle->timeout = g_timeout_add (delay, send_response, handle); + + return TRUE; +} + +void +wallpaper_init (GDBusConnection *connection, + const char *object_path) +{ + g_autoptr(GError) error = NULL; + GDBusInterfaceSkeleton *helper; + + helper = G_DBUS_INTERFACE_SKELETON (xdp_dbus_impl_wallpaper_skeleton_new ()); + + g_signal_connect (helper, "handle-set-wallpaper-uri", G_CALLBACK (handle_set_wallpaper_uri), NULL); + + if (!g_dbus_interface_skeleton_export (helper, connection, object_path, &error)) + { + g_error ("Failed to export %s skeleton: %s\n", + g_dbus_interface_skeleton_get_info (helper)->name, + error->message); + exit (1); + } + + g_debug ("providing %s at %s", g_dbus_interface_skeleton_get_info (helper)->name, object_path); +} diff --git a/tests/backend/wallpaper.h b/tests/backend/wallpaper.h new file mode 100644 index 0000000..7f76586 --- /dev/null +++ b/tests/backend/wallpaper.h @@ -0,0 +1,3 @@ +#pragma once + +void wallpaper_init (GDBusConnection *connection, const char *object_path); diff --git a/tests/background.c b/tests/background.c new file mode 100644 index 0000000..8f9223b --- /dev/null +++ b/tests/background.c @@ -0,0 +1,208 @@ +#include + +#include "background.h" + +#include +#include "xdp-utils.h" + +extern char outdir[]; + +static int got_info; + +static void +background_cb (GObject *object, + GAsyncResult *result, + gpointer data) +{ + XdpPortal *portal = XDP_PORTAL (object); + g_autoptr(GError) error = NULL; + gboolean res; + + res = xdp_portal_request_background_finish (portal, result, &error); + g_assert_true (res); + g_assert_no_error (error); + + got_info = 1; +} + +void +test_background_basic1 (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GPtrArray) argv = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autofree char *path = NULL; + g_autoptr(GError) error = NULL; + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + + path = g_build_filename (outdir, "access", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + g_free (path); + + g_key_file_unref (keyfile); + keyfile = g_key_file_new (); + + g_key_file_set_string (keyfile, "background", "reason", "Testing portals"); + g_key_file_set_boolean (keyfile, "background", "autostart", FALSE); + g_key_file_set_boolean (keyfile, "background", "dbus_activatable", FALSE); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + + path = g_build_filename (outdir, "background", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + argv = g_ptr_array_new (); + g_ptr_array_add (argv, "/bin/true"); + + xdp_portal_request_background (portal, NULL, "Testing portals", argv, 0, NULL, background_cb, NULL); + + while (got_info < 1) + g_main_context_iteration (NULL, TRUE); +} + +void +test_background_basic2 (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GPtrArray) argv = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autofree char *path = NULL; + g_autoptr(GError) error = NULL; + XdpBackgroundFlags flags = XDP_BACKGROUND_FLAG_NONE; + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + + path = g_build_filename (outdir, "access", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + g_free (path); + + g_key_file_unref (keyfile); + keyfile = g_key_file_new (); + + g_key_file_set_string (keyfile, "background", "reason", "Testing portals"); + g_key_file_set_boolean (keyfile, "background", "autostart", TRUE); + g_key_file_set_boolean (keyfile, "background", "dbus_activatable", TRUE); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + + path = g_build_filename (outdir, "background", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + argv = g_ptr_array_new (); + g_ptr_array_add (argv, "/bin/true"); + + flags = XDP_BACKGROUND_FLAG_AUTOSTART | XDP_BACKGROUND_FLAG_ACTIVATABLE; + xdp_portal_request_background (portal, NULL, "Testing portals", argv, flags, NULL, background_cb, NULL); + + while (got_info < 1) + g_main_context_iteration (NULL, TRUE); +} + +static void +background_fail (GObject *object, + GAsyncResult *result, + gpointer data) +{ + XdpPortal *portal = XDP_PORTAL (object); + g_autoptr(GError) error = NULL; + gboolean res; + + res = xdp_portal_request_background_finish (portal, result, &error); + g_assert_false (res); + g_assert_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT); + + got_info = 1; +} + +void +test_background_commandline (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GPtrArray) argv = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autofree char *path = NULL; + g_autoptr(GError) error = NULL; + XdpBackgroundFlags flags = XDP_BACKGROUND_FLAG_NONE; + + keyfile = g_key_file_new (); + + g_key_file_set_string (keyfile, "background", "reason", "Testing portals"); + g_key_file_set_boolean (keyfile, "background", "autostart", TRUE); + g_key_file_set_boolean (keyfile, "background", "dbus_activatable", TRUE); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + + path = g_build_filename (outdir, "background", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + argv = g_ptr_array_new (); + + flags = XDP_BACKGROUND_FLAG_AUTOSTART | XDP_BACKGROUND_FLAG_ACTIVATABLE; + xdp_portal_request_background (portal, NULL, "Testing portals", argv, flags, NULL, background_fail, NULL); + + while (got_info < 1) + g_main_context_iteration (NULL, TRUE); +} + +void +test_background_reason (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GPtrArray) argv = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autofree char *path = NULL; + g_autoptr(GError) error = NULL; + XdpBackgroundFlags flags = XDP_BACKGROUND_FLAG_NONE; + + keyfile = g_key_file_new (); + + g_key_file_set_string (keyfile, "background", "reason", "Testing portals"); + g_key_file_set_boolean (keyfile, "background", "autostart", TRUE); + g_key_file_set_boolean (keyfile, "background", "dbus_activatable", TRUE); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + + path = g_build_filename (outdir, "background", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + argv = g_ptr_array_new (); + + flags = XDP_BACKGROUND_FLAG_AUTOSTART | XDP_BACKGROUND_FLAG_ACTIVATABLE; + xdp_portal_request_background (portal, NULL, +"012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789" +"012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789" +"012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789", +argv, flags, NULL, background_fail, NULL); + + while (got_info < 1) + g_main_context_iteration (NULL, TRUE); +} diff --git a/tests/background.h b/tests/background.h new file mode 100644 index 0000000..9d87810 --- /dev/null +++ b/tests/background.h @@ -0,0 +1,7 @@ +#pragma once + +void test_background_basic1 (void); +void test_background_basic2 (void); +void test_background_commandline (void); +void test_background_reason (void); + diff --git a/tests/camera.c b/tests/camera.c new file mode 100644 index 0000000..c590af9 --- /dev/null +++ b/tests/camera.c @@ -0,0 +1,350 @@ +#include + +#include "camera.h" + +#include +#include "xdp-utils.h" +#include "xdp-impl-dbus.h" + +#include "utils.h" + +extern char outdir[]; + +static int got_info; + +extern XdpDbusImplPermissionStore *permission_store; +extern XdpDbusImplLockdown *lockdown; + +static void +set_camera_permissions (const char *permission) +{ + const char *permissions[2] = { NULL, NULL }; + g_autoptr(GError) error = NULL; + + permissions[0] = permission; + xdp_dbus_impl_permission_store_call_set_permission_sync (permission_store, + "devices", + TRUE, + "camera", + "", + permissions, + NULL, + &error); + g_assert_no_error (error); +} + +static void +reset_camera_permissions (void) +{ + set_camera_permissions (NULL); +} + +static void +camera_cb (GObject *obj, + GAsyncResult *result, + gpointer data) +{ + XdpPortal *portal = XDP_PORTAL (obj); + g_autoptr(GError) error = NULL; + GKeyFile *keyfile = data; + int response; + int domain; + int code; + gboolean ret; + + response = g_key_file_get_integer (keyfile, "result", "response", NULL); + domain = g_key_file_get_integer (keyfile, "result", "error_domain", NULL); + code = g_key_file_get_integer (keyfile, "result", "error_code", NULL); + + ret = xdp_portal_access_camera_finish (portal, result, &error); + + g_debug ("camera cb: %d", g_key_file_get_integer (keyfile, "result", "marker", NULL)); + if (response == 0) + { + g_assert_true (ret); + g_assert_no_error (error); + } + else if (response == 1) + { + g_assert_false (ret); + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED); + } + else if (response == 2) + { + g_assert_false (ret); + g_assert_error (error, domain, code); + } + else + g_assert_not_reached (); + + got_info++; + + g_main_context_wakeup (NULL); +} + +void +test_camera_basic (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + reset_camera_permissions (); + + keyfile = g_key_file_new (); + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "access", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_access_camera (portal, NULL, 0, NULL, camera_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_camera_delay (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + reset_camera_permissions (); + + keyfile = g_key_file_new (); + g_key_file_set_integer (keyfile, "result", "marker", 1); + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "access", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_access_camera (portal, NULL, 0, NULL, camera_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_camera_cancel (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + reset_camera_permissions (); + + keyfile = g_key_file_new (); + g_key_file_set_integer (keyfile, "result", "marker", 2); + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 1); + g_key_file_set_integer (keyfile, "result", "response", 1); + + path = g_build_filename (outdir, "access", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_access_camera (portal, NULL, 0, NULL, camera_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +static gboolean +cancel_call (gpointer data) +{ + GCancellable *cancellable = data; + + g_debug ("cancel call"); + g_cancellable_cancel (cancellable); + + return G_SOURCE_REMOVE; +} + +void +test_camera_close (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + g_autoptr(GCancellable) cancellable = NULL; + + reset_camera_permissions (); + + keyfile = g_key_file_new (); + g_key_file_set_integer (keyfile, "result", "marker", 3); + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + //g_key_file_set_boolean (keyfile, "backend", "expect-close", 1); + g_key_file_set_integer (keyfile, "result", "response", 1); + + path = g_build_filename (outdir, "access", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + cancellable = g_cancellable_new (); + + got_info = 0; + xdp_portal_access_camera (portal, NULL, 0, cancellable, camera_cb, keyfile); + + g_timeout_add (100, cancel_call, cancellable); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_camera_lockdown (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + reset_camera_permissions (); + + tests_set_property_sync (G_DBUS_PROXY (lockdown), + "org.freedesktop.impl.portal.Lockdown", + "disable-camera", + g_variant_new_boolean (TRUE), + &error); + g_assert_no_error (error); + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "result", "marker", 4); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 2); + g_key_file_set_integer (keyfile, "result", "error_domain", XDG_DESKTOP_PORTAL_ERROR); + g_key_file_set_integer (keyfile, "result", "error_code", XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED); + + path = g_build_filename (outdir, "access", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_access_camera (portal, NULL, 0, NULL, camera_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); + + tests_set_property_sync (G_DBUS_PROXY (lockdown), + "org.freedesktop.impl.portal.Lockdown", + "disable-camera", + g_variant_new_boolean (FALSE), + &error); + g_assert_no_error (error); +} + +/* Test the effect of the user denying the access dialog */ +void +test_camera_no_access1 (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + reset_camera_permissions (); + + keyfile = g_key_file_new (); + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 2); + g_key_file_set_integer (keyfile, "result", "response", 1); + + path = g_build_filename (outdir, "access", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_access_camera (portal, NULL, 0, NULL, camera_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +/* Test the effect of the permissions being stored */ +void +test_camera_no_access2 (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + set_camera_permissions ("no"); + + keyfile = g_key_file_new (); + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 1); + + path = g_build_filename (outdir, "access", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_access_camera (portal, NULL, 0, NULL, camera_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_camera_parallel (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + reset_camera_permissions (); + + keyfile = g_key_file_new (); + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "access", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_access_camera (portal, NULL, 0, NULL, camera_cb, keyfile); + xdp_portal_access_camera (portal, NULL, 0, NULL, camera_cb, keyfile); + xdp_portal_access_camera (portal, NULL, 0, NULL, camera_cb, keyfile); + + while (got_info < 3) + g_main_context_iteration (NULL, TRUE); +} + diff --git a/tests/camera.h b/tests/camera.h new file mode 100644 index 0000000..86c9034 --- /dev/null +++ b/tests/camera.h @@ -0,0 +1,10 @@ +#pragma once + +void test_camera_basic (void); +void test_camera_delay (void); +void test_camera_cancel (void); +void test_camera_close (void); +void test_camera_lockdown (void); +void test_camera_no_access1 (void); +void test_camera_no_access2 (void); +void test_camera_parallel (void); diff --git a/tests/can-use-fuse.c b/tests/can-use-fuse.c new file mode 100644 index 0000000..d7f7b65 --- /dev/null +++ b/tests/can-use-fuse.c @@ -0,0 +1,138 @@ +/* + * Copyright 2019-2021 Collabora Ltd. + * Copyright 2021 Canonical Ltd. + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "config.h" + +#include "can-use-fuse.h" + +#include +#include + +#include + +#define FUSE_USE_VERSION 35 +#include + +gchar *cannot_use_fuse = NULL; + +static void pc_init(void *userdata, + struct fuse_conn_info *conn) +{ + struct fuse_session **session_ptr = userdata; + + if (!(conn->capable & FUSE_CAP_SPLICE_READ)) + cannot_use_fuse = g_strdup ("FUSE_CAP_SPLICE_READ"); + else if (!(conn->capable & FUSE_CAP_SPLICE_WRITE)) + cannot_use_fuse = g_strdup ("Missing FUSE_CAP_SPLICE_WRITE"); + else if (!(conn->capable & FUSE_CAP_SPLICE_MOVE)) + cannot_use_fuse = g_strdup ("Missing FUSE_CAP_SPLICE_MOVE"); + else if (!(conn->capable & FUSE_CAP_ATOMIC_O_TRUNC)) + cannot_use_fuse = g_strdup ("Missing FUSE_CAP_ATOMIC_O_TRUNC"); + + fuse_session_exit (*session_ptr); +} + +/* + * If we cannot use FUSE, set cannot_use_fuse and return %FALSE. + */ +gboolean +check_fuse (void) +{ + g_autofree gchar *fusermount = NULL; + g_autofree gchar *path = NULL; + char *argv[] = { "flatpak-fuse-test" }; + const struct fuse_lowlevel_ops pc_oper = { .init = pc_init }; + struct fuse_args args = FUSE_ARGS_INIT (G_N_ELEMENTS (argv), argv); + struct fuse_session *session = NULL; + g_autoptr(GError) error = NULL; + + if (cannot_use_fuse != NULL) + return FALSE; + + if (access ("/dev/fuse", W_OK) != 0) + { + cannot_use_fuse = g_strdup_printf ("access /dev/fuse: %s", + g_strerror (errno)); + return FALSE; + } + + fusermount = g_find_program_in_path ("fusermount3"); + + if (fusermount == NULL) + { + cannot_use_fuse = g_strdup ("fusermount3 not found in PATH"); + return FALSE; + } + + if (!g_file_test (fusermount, G_FILE_TEST_IS_EXECUTABLE)) + { + cannot_use_fuse = g_strdup_printf ("%s not executable", fusermount); + return FALSE; + } + + if (!g_file_test ("/etc/mtab", G_FILE_TEST_EXISTS)) + { + cannot_use_fuse = g_strdup ("fusermount3 won't work without /etc/mtab"); + return FALSE; + } + + path = g_dir_make_tmp ("flatpak-test.XXXXXX", &error); + g_assert_no_error (error); + + session = fuse_session_new (&args, &pc_oper, sizeof (pc_oper), &session); + + if (session == NULL) + { + fuse_opt_free_args (&args); + cannot_use_fuse = g_strdup_printf ("fuse_mount: %s", + g_strerror (errno)); + return FALSE; + } + + if (fuse_session_mount (session, path) != 0) + { + fuse_opt_free_args (&args); + fuse_session_destroy (session); + cannot_use_fuse = g_strdup_printf ("fuse_mount: impossible to mount path " + "'%s': %s", + path, g_strerror (errno)); + return FALSE; + } + + g_assert (cannot_use_fuse == NULL); + fuse_session_loop (session); + + if (cannot_use_fuse != NULL) + { + fuse_opt_free_args (&args); + fuse_session_destroy (session); + return FALSE; + } + + g_test_message ("Successfully set up test FUSE fs on %s", path); + fuse_session_unmount (session); + + if (g_rmdir (path) != 0) + g_error ("rmdir %s: %s", path, g_strerror (errno)); + + fuse_opt_free_args (&args); + fuse_session_destroy (session); + + return TRUE; +} + +gboolean +check_fuse_or_skip_test (void) +{ + if (!check_fuse ()) + { + g_assert (cannot_use_fuse != NULL); + g_test_skip (cannot_use_fuse); + return FALSE; + } + + return TRUE; +} diff --git a/tests/can-use-fuse.h b/tests/can-use-fuse.h new file mode 100644 index 0000000..bfc47e3 --- /dev/null +++ b/tests/can-use-fuse.h @@ -0,0 +1,12 @@ +/* + * Copyright 2019-2021 Collabora Ltd. + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#include + +extern gchar *cannot_use_fuse; +gboolean check_fuse (void); +gboolean check_fuse_or_skip_test (void); diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..fac8c78 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,78 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is formatted with Python Black + +from typing import Any, Iterator + +import pytest +import dbus +import dbusmock + +from tests import PortalMock + + +class SessionBusMock(dbusmock.DBusTestCase): + def __init__(self): + super().__init__() + self._dbus_con = None + + @property + def dbus_con(self) -> "dbus.Bus": + return self._dbus_con + + +@pytest.fixture() +def session_bus() -> Iterator[SessionBusMock]: + """ + Fixture to yield a DBusTestCase with a started session bus. + """ + bus = SessionBusMock() + bus.setUp() + bus.start_session_bus() + con = bus.get_dbus(system_bus=False) + assert con + bus._dbus_con = con + yield bus + bus.tearDown() + bus.tearDownClass() + + +@pytest.fixture +def portal_name() -> str: + raise NotImplementedError("All test files need to define the portal_name fixture") + + +@pytest.fixture +def portal_has_impl() -> bool: + """ + Default fixture for signaling that a portal has an impl.portal as well. + + For tests of portals that do not have an impl, override this fixture to + return False in the respective test_foo.py. + """ + return True + + +@pytest.fixture +def params() -> dict[str, Any]: + """ + Default fixture providing empty parameters that get passed to the impl.portal. + To use this in test cases, pass the parameters via + + @pytest.mark.parametrize("params", ({"foo": "bar"}, )) + + Note that this must be a tuple as pytest will iterate over the value. + """ + return {} + + +@pytest.fixture +def portal_mock(session_bus, portal_name, params, portal_has_impl) -> PortalMock: + """ + Fixture yielding a PortalMock object with the impl started, if applicable. + """ + pmock = PortalMock(session_bus, portal_name) + if portal_has_impl: + pmock.start_impl_portal(params) + pmock.start_xdp() + return pmock diff --git a/tests/dbs/meson.build b/tests/dbs/meson.build new file mode 100644 index 0000000..b9896c4 --- /dev/null +++ b/tests/dbs/meson.build @@ -0,0 +1,5 @@ +# FIXME: should be installed if installed_tests +dbs = configure_file(input: 'no_tables', + output: '@PLAINNAME@', + copy: true +) diff --git a/tests/dbs/no_tables b/tests/dbs/no_tables new file mode 100644 index 0000000..c700bdb Binary files /dev/null and b/tests/dbs/no_tables differ diff --git a/tests/email.c b/tests/email.c new file mode 100644 index 0000000..b6bac8c --- /dev/null +++ b/tests/email.c @@ -0,0 +1,420 @@ +#include + +#include "email.h" + +#include +#include "xdp-utils.h" + +extern char outdir[]; + +static int got_info; + +static void +email_cb (GObject *obj, + GAsyncResult *result, + gpointer data) +{ + XdpPortal *portal = XDP_PORTAL (obj); + g_autoptr(GError) error = NULL; + gboolean ret; + GKeyFile *keyfile = data; + int response; + int domain; + int code; + + response = g_key_file_get_integer (keyfile, "result", "response", NULL); + domain = g_key_file_get_integer (keyfile, "result", "error_domain", NULL); + code = g_key_file_get_integer (keyfile, "result", "error_code", NULL); + + ret = xdp_portal_compose_email_finish (portal, result, &error); + g_assert (ret == (response == 0)); + if (response == 0) + g_assert_no_error (error); + else if (response == 1) + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED); + else if (response == 2) + g_assert_error (error, domain, code); + else + g_assert_not_reached (); + + got_info++; + + g_main_context_wakeup (NULL); +} + +/* some basic tests using libportal, and test that communication + * with the backend via keyfile works + */ +void +test_email_basic (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + const char *addresses[2] = { "mclasen@redhat.com", NULL }; + + keyfile = g_key_file_new (); + + g_key_file_set_string (keyfile, "input", "address", "mclasen@redhat.com"); + g_key_file_set_string (keyfile, "input", "subject", "Re: portal tests"); + g_key_file_set_string (keyfile, "input", "body", "You have to see this..."); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "email", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_compose_email (portal, NULL, + addresses, NULL, NULL, + "Re: portal tests", + "You have to see this...", + NULL, + 0, + NULL, + email_cb, + keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +/* test that an invalid address triggers an error + */ +void +test_email_address (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + const char *addresses[2];; + + keyfile = g_key_file_new (); + + addresses[0] = "gibberish! not an email address\n%Q"; + addresses[1] = NULL; + + g_key_file_set_string_list (keyfile, "input", "addresses", addresses, 1); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 2); + g_key_file_set_integer (keyfile, "result", "error_domain", XDG_DESKTOP_PORTAL_ERROR); + g_key_file_set_integer (keyfile, "result", "error_code", XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT); + + path = g_build_filename (outdir, "email", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_compose_email (portal, NULL, + addresses, NULL, NULL, + NULL, + NULL, + NULL, + 0, + NULL, + email_cb, + keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +/* test that an invalid subject triggers an error + */ +void +test_email_subject (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + const char *subject; + + keyfile = g_key_file_new (); + + subject = "not\na\nvalid\nsubject line"; + + g_key_file_set_string (keyfile, "input", "subject", subject); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 2); + g_key_file_set_integer (keyfile, "result", "error_domain", XDG_DESKTOP_PORTAL_ERROR); + g_key_file_set_integer (keyfile, "result", "error_code", XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT); + + path = g_build_filename (outdir, "email", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_compose_email (portal, NULL, + NULL, NULL, NULL, + subject, + NULL, + NULL, + 0, + NULL, + email_cb, + keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); + + subject = "This subject line is too long, much too long. It is more than twohundred characters long, which is much, much too long for a reasonable subject line. Be concise! This is not twitter where you can use hundreds of characters, including Emoji like 😂️ or 😩️"; + g_assert_cmpint (g_utf8_strlen (subject, -1), >, 200); + + g_key_file_set_string (keyfile, "input", "subject", subject); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + got_info = 0; + xdp_portal_compose_email (portal, NULL, + NULL, NULL, NULL, + subject, + NULL, + NULL, + 0, + NULL, + email_cb, + keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +/* test that everything works as expected when the + * backend takes some time to send its response, as + * is to be expected from a real backend that presents + * dialogs to the user. + */ +void +test_email_delay (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + const char *addresses[2]; + const char *subject; + + addresses[0] = "mclasen@redhat.com"; + addresses[1] = NULL; + subject = "delay test"; + + keyfile = g_key_file_new (); + g_key_file_set_string_list (keyfile, "input", "addresses", addresses, 1); + g_key_file_set_string (keyfile, "input", "subject", subject); + + g_key_file_set_integer (keyfile, "backend", "delay", 400); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "email", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_compose_email (portal, NULL, + addresses, NULL, NULL, + subject, + NULL, + NULL, + 0, + NULL, + email_cb, + keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +/* Test that user cancellation works as expected. + * We simulate that the user cancels a hypothetical dialog, + * by telling the backend to return 1 as response code. + * And we check that we get the expected G_IO_ERROR_CANCELLED. + */ +void +test_email_cancel (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + const char *addresses[2]; + const char *subject; + + addresses[0] = "mclasen@redhat.com"; + addresses[1] = NULL; + subject = "delay test"; + + keyfile = g_key_file_new (); + g_key_file_set_string_list (keyfile, "input", "addresses", addresses, 1); + g_key_file_set_string (keyfile, "input", "subject", subject); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 1); + g_key_file_set_integer (keyfile, "result", "response", 1); + + path = g_build_filename (outdir, "email", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_compose_email (portal, NULL, + addresses, NULL, NULL, + subject, + NULL, + NULL, + 0, + NULL, + email_cb, + keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +static gboolean +cancel_call (gpointer data) +{ + GCancellable *cancellable = data; + + g_debug ("cancel call"); + g_cancellable_cancel (cancellable); + + return G_SOURCE_REMOVE; +} + +/* Test that app-side cancellation works as expected. + * We cancel the cancellable while while the hypothetical + * dialog is up, and tell the backend that it should + * expect a Close call. We rely on the backend to + * verify that that call actually happened. + */ +void +test_email_close (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + g_autoptr(GCancellable) cancellable = NULL; + const char *addresses[2]; + const char *subject; + + addresses[0] = "mclasen@redhat.com"; + addresses[1] = NULL; + subject = "delay test"; + + keyfile = g_key_file_new (); + g_key_file_set_string_list (keyfile, "input", "addresses", addresses, 1); + g_key_file_set_string (keyfile, "input", "subject", subject); + + g_key_file_set_integer (keyfile, "backend", "delay", 400); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_boolean (keyfile, "backend", "expect-close", 1); + g_key_file_set_integer (keyfile, "result", "response", 1); + + path = g_build_filename (outdir, "email", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + cancellable = g_cancellable_new (); + + got_info = 0; + xdp_portal_compose_email (portal, NULL, + addresses, NULL, NULL, + subject, + NULL, + NULL, + 0, + cancellable, + email_cb, + keyfile); + + g_timeout_add (100, cancel_call, cancellable); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_email_parallel (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + const char *addresses[2]; + + addresses[0] = "mclasen@redhat.com"; + addresses[1] = NULL; + + keyfile = g_key_file_new (); + + g_key_file_set_string_list (keyfile, "input", "addresses", addresses, 1); + g_key_file_set_string (keyfile, "input", "subject", "Re: portal tests"); + g_key_file_set_string (keyfile, "input", "body", "You have to see this..."); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "email", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_compose_email (portal, NULL, + addresses, NULL, NULL, + "Re: portal tests", + "You have to see this...", + NULL, + 0, + NULL, + email_cb, + keyfile); + xdp_portal_compose_email (portal, NULL, + addresses, NULL, NULL, + "Re: portal tests", + "You have to see this...", + NULL, + 0, + NULL, + email_cb, + keyfile); + xdp_portal_compose_email (portal, NULL, + addresses, NULL, NULL, + "Re: portal tests", + "You have to see this...", + NULL, + 0, + NULL, + email_cb, + keyfile); + + while (got_info < 3) + g_main_context_iteration (NULL, TRUE); +} + diff --git a/tests/email.h b/tests/email.h new file mode 100644 index 0000000..db4f955 --- /dev/null +++ b/tests/email.h @@ -0,0 +1,9 @@ +#pragma once + +void test_email_basic (void); +void test_email_address (void); +void test_email_subject (void); +void test_email_delay (void); +void test_email_cancel (void); +void test_email_close (void); +void test_email_parallel (void); diff --git a/tests/filechooser.c b/tests/filechooser.c new file mode 100644 index 0000000..bba88c3 --- /dev/null +++ b/tests/filechooser.c @@ -0,0 +1,945 @@ +#include + +#include "account.h" + +#include "glib-backports.h" + +#include +#include "xdp-utils.h" +#include "xdp-impl-dbus.h" + +#include "utils.h" + +extern XdpDbusImplLockdown *lockdown; + +extern char outdir[]; + +static int got_info; + +static void +open_file_cb (GObject *obj, + GAsyncResult *result, + gpointer data) +{ + XdpPortal *portal = XDP_PORTAL (obj); + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) ret = NULL; + GKeyFile *keyfile = data; + int response; + int domain; + int code; + + response = g_key_file_get_integer (keyfile, "result", "response", NULL); + domain = g_key_file_get_integer (keyfile, "result", "error_domain", NULL); + code = g_key_file_get_integer (keyfile, "result", "error_code", NULL); + + ret = xdp_portal_open_file_finish (portal, result, &error); + if (response == 0) + { + const char * const *uris; + g_auto(GStrv) expected_uris = NULL; + g_autofree char *expected_choices = NULL; + g_autoptr(GVariant) choices = NULL; + + g_assert_no_error (error); + g_variant_lookup (ret, "uris", "^a&s", &uris); + expected_uris = g_key_file_get_string_list (keyfile, "result", "uris", NULL, NULL); + + g_assert (g_strv_equal (uris, (const char * const *)expected_uris)); + + expected_choices = g_key_file_get_string (keyfile, "result", "choices", NULL); + g_variant_lookup (ret, "choices", "@a(ss)", &choices); + if (expected_choices) + { + g_autoptr(GVariant) c = NULL; + g_assert_nonnull (choices); + c = g_variant_parse (G_VARIANT_TYPE ("a(ss)"), expected_choices, NULL, NULL, NULL); + g_assert_true (g_variant_equal (choices, c)); + } + else + { + g_assert_null (choices); + } + } + else if (response == 1) + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED); + else if (response == 2) + g_assert_error (error, domain, code); + else + g_assert_not_reached (); + + got_info++; + + g_main_context_wakeup (NULL); +} + +void +test_open_file_basic (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + const char * uris[] = { + "file:///test/file", + NULL + }; + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + g_key_file_set_string_list (keyfile, "result", "uris", uris, g_strv_length ((char **)uris)); + + path = g_build_filename (outdir, "filechooser", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_open_file (portal, NULL, "test", NULL, NULL, NULL, 0, NULL, open_file_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_open_file_delay (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + const char * uris[] = { + "file:///test/file", + NULL + }; + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + g_key_file_set_string_list (keyfile, "result", "uris", uris, g_strv_length ((char **)uris)); + + path = g_build_filename (outdir, "filechooser", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_open_file (portal, NULL, "test", NULL, NULL, NULL, 0, NULL, open_file_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_open_file_cancel (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + const char * uris[] = { + "file:///test/file", + NULL + }; + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 1); + g_key_file_set_integer (keyfile, "result", "response", 1); + g_key_file_set_string_list (keyfile, "result", "uris", uris, g_strv_length ((char **)uris)); + + path = g_build_filename (outdir, "filechooser", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_open_file (portal, NULL, "test", NULL, NULL, NULL, 0, NULL, open_file_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +static gboolean +cancel_call (gpointer data) +{ + GCancellable *cancellable = data; + + g_debug ("cancel call"); + g_cancellable_cancel (cancellable); + + return G_SOURCE_REMOVE; +} + +void +test_open_file_close (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GCancellable) cancellable = NULL; + g_autofree char *path = NULL; + const char * uris[] = { + "file:///test/file", + NULL + }; + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "expect-close", 1); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 1); + g_key_file_set_string_list (keyfile, "result", "uris", uris, g_strv_length ((char **)uris)); + + path = g_build_filename (outdir, "filechooser", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + cancellable = g_cancellable_new (); + + got_info = 0; + xdp_portal_open_file (portal, NULL, "test", NULL, NULL, NULL, 0, cancellable, open_file_cb, keyfile); + + g_timeout_add (100, cancel_call, cancellable); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_open_file_multiple (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + const char * uris[] = { + "file:///test/file1", + "file:///test/file2", + NULL + }; + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + g_key_file_set_string_list (keyfile, "result", "uris", uris, g_strv_length ((char **)uris)); + + path = g_build_filename (outdir, "filechooser", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_open_file (portal, NULL, "test", NULL, NULL, NULL, 0, NULL, open_file_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_open_file_filters1 (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + const char * uris[] = { + "file:///test/file1", + NULL + }; + g_autoptr(GVariant) filters = NULL; + const char *filter_string = + "[('Images', [(0, '*ico'), (1, 'image/png')]), ('Text', [(0, '*.txt')])]"; + + filters = g_variant_parse (G_VARIANT_TYPE ("a(sa(us))"), filter_string, NULL, NULL, &error); + + keyfile = g_key_file_new (); + + g_assert_no_error (error); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_string (keyfile, "backend", "filters", filter_string); + g_key_file_set_integer (keyfile, "result", "response", 0); + g_key_file_set_string_list (keyfile, "result", "uris", uris, g_strv_length ((char **)uris)); + + path = g_build_filename (outdir, "filechooser", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_open_file (portal, NULL, "test", filters, NULL, NULL, 0, NULL, open_file_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_open_file_filters2 (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + const char * uris[] = { + "file:///test/file1", + NULL + }; + g_autoptr(GVariant) filters = NULL; + const char *filter_string = + "[('Images', [(0, '*ico'), (1, 'image/png')]), ('Text', [(4, '*.txt')])]"; /* invalid type */ + + filters = g_variant_parse (G_VARIANT_TYPE ("a(sa(us))"), filter_string, NULL, NULL, &error); + + keyfile = g_key_file_new (); + + g_assert_no_error (error); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_string (keyfile, "backend", "filters", filter_string); + g_key_file_set_integer (keyfile, "result", "response", 2); + g_key_file_set_integer (keyfile, "result", "error_domain", XDG_DESKTOP_PORTAL_ERROR); + g_key_file_set_integer (keyfile, "result", "error_code", XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT); + g_key_file_set_string_list (keyfile, "result", "uris", uris, g_strv_length ((char **)uris)); + + path = g_build_filename (outdir, "filechooser", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_open_file (portal, NULL, "test", filters, NULL, NULL, 0, NULL, open_file_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_open_file_current_filter1 (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + const char * uris[] = { + "file:///test/file1", + NULL + }; + g_autoptr(GVariant) filters = NULL; + g_autoptr(GVariant) current_filter = NULL; + const char *filter_string = "[('Images', [(0, '*ico'), (1, 'image/png')]), ('Text', [(0, '*.txt')])]"; + const char *current_filter_string = "('Text', [(0, '*.txt')])"; + + filters = g_variant_parse (G_VARIANT_TYPE ("a(sa(us))"), filter_string, NULL, NULL, &error); + g_assert_no_error (error); + + current_filter = g_variant_parse (G_VARIANT_TYPE ("(sa(us))"), current_filter_string, NULL, NULL, &error); + g_assert_no_error (error); + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_string (keyfile, "backend", "filters", filter_string); + g_key_file_set_string (keyfile, "backend", "current_filter", current_filter_string); + g_key_file_set_integer (keyfile, "result", "response", 0); + g_key_file_set_string_list (keyfile, "result", "uris", uris, g_strv_length ((char **)uris)); + + path = g_build_filename (outdir, "filechooser", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_open_file (portal, NULL, "test", filters, current_filter, NULL, 0, NULL, open_file_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_open_file_current_filter2 (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + const char * uris[] = { + "file:///test/file1", + NULL + }; + g_autoptr(GVariant) current_filter = NULL; + const char *current_filter_string = "('Text', [(0, '*.txt')])"; + + current_filter = g_variant_parse (G_VARIANT_TYPE ("(sa(us))"), current_filter_string, NULL, NULL, &error); + g_assert_no_error (error); + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_string (keyfile, "backend", "current_filter", current_filter_string); + g_key_file_set_integer (keyfile, "result", "response", 0); + g_key_file_set_string_list (keyfile, "result", "uris", uris, g_strv_length ((char **)uris)); + + path = g_build_filename (outdir, "filechooser", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_open_file (portal, NULL, "test", NULL, current_filter, NULL, 0, NULL, open_file_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_open_file_current_filter3 (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + const char * uris[] = { + "file:///test/file1", + NULL + }; + g_autoptr(GVariant) current_filter = NULL; + const char *current_filter_string = "('Text', [(6, '*.txt')])"; /* invalid type */ + + current_filter = g_variant_parse (G_VARIANT_TYPE ("(sa(us))"), current_filter_string, NULL, NULL, &error); + g_assert_no_error (error); + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_string (keyfile, "backend", "current_filter", current_filter_string); + g_key_file_set_integer (keyfile, "result", "response", 2); + g_key_file_set_integer (keyfile, "result", "error_domain", XDG_DESKTOP_PORTAL_ERROR); + g_key_file_set_integer (keyfile, "result", "error_code", XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT); + g_key_file_set_string_list (keyfile, "result", "uris", uris, g_strv_length ((char **)uris)); + + path = g_build_filename (outdir, "filechooser", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_open_file (portal, NULL, "test", NULL, current_filter, NULL, 0, NULL, open_file_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_open_file_current_filter4 (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + const char * uris[] = { + "file:///test/file1", + NULL + }; + g_autoptr(GVariant) filters = NULL; + g_autoptr(GVariant) current_filter = NULL; + const char *filter_string = "[('Images', [(0, '*ico'), (1, 'image/png')]), ('Text', [(0, '*.txt')])]"; + const char *current_filter_string = "('Something else', [(0, '*.sth.else')])"; /* not in the list */ + + filters = g_variant_parse (G_VARIANT_TYPE ("a(sa(us))"), filter_string, NULL, NULL, &error); + g_assert_no_error (error); + + current_filter = g_variant_parse (G_VARIANT_TYPE ("(sa(us))"), current_filter_string, NULL, NULL, &error); + g_assert_no_error (error); + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_string (keyfile, "backend", "filters", filter_string); + g_key_file_set_string (keyfile, "backend", "current_filter", current_filter_string); + g_key_file_set_integer (keyfile, "result", "response", 2); + g_key_file_set_integer (keyfile, "result", "error_domain", XDG_DESKTOP_PORTAL_ERROR); + g_key_file_set_integer (keyfile, "result", "error_code", XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT); + g_key_file_set_string_list (keyfile, "result", "uris", uris, g_strv_length ((char **)uris)); + + path = g_build_filename (outdir, "filechooser", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_open_file (portal, NULL, "test", filters, current_filter, NULL, 0, NULL, open_file_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_open_file_choices1 (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + const char * uris[] = { + "file:///test/file1", + NULL + }; + g_autoptr(GVariant) choices = NULL; + const char *choices_string = "[('encoding', 'Encoding', [('utf8', 'Unicode'), ('latin15', 'Western')], 'latin15'), ('reencode', 'Reencode', [], 'false'), ('third', 'Third', [('a', 'A'), ('b', 'B')], '')]"; + const char *chosen_string = "[('encoding', 'utf8'), ('reencode', 'true'), ('third', 'a')]"; + + choices = g_variant_parse (G_VARIANT_TYPE ("a(ssa(ss)s)"), choices_string, NULL, NULL, &error); + g_assert_no_error (error); + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_string (keyfile, "backend", "choices", choices_string); + g_key_file_set_integer (keyfile, "result", "response", 0); + g_key_file_set_string (keyfile, "result", "choices", chosen_string); + g_key_file_set_string_list (keyfile, "result", "uris", uris, g_strv_length ((char **)uris)); + + path = g_build_filename (outdir, "filechooser", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_open_file (portal, NULL, "test", NULL, NULL, choices, 0, NULL, open_file_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_open_file_choices2 (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + g_autoptr(GVariant) choices = NULL; + const char *choices_string = "[('encoding', 'Encoding', [('utf8', ''), ('latin15', 'Western')], 'latin15'), ('reencode', 'Reencode', [], 'false')]"; /* invalid: empty label */ + + choices = g_variant_parse (G_VARIANT_TYPE ("a(ssa(ss)s)"), choices_string, NULL, NULL, &error); + g_assert_no_error (error); + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_string (keyfile, "backend", "choices", choices_string); + g_key_file_set_integer (keyfile, "result", "response", 2); + g_key_file_set_integer (keyfile, "result", "error_domain", XDG_DESKTOP_PORTAL_ERROR); + g_key_file_set_integer (keyfile, "result", "error_code", XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT); + + path = g_build_filename (outdir, "filechooser", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_open_file (portal, NULL, "test", NULL, NULL, choices, 0, NULL, open_file_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_open_file_choices3 (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + g_autoptr(GVariant) choices = NULL; + const char *choices_string = "[('', 'Encoding', [('utf8', 'Unicode'), ('latin15', 'Western')], 'latin15'), ('reencode', 'Reencode', [], 'false')]"; /* invalid: empty id */ + + choices = g_variant_parse (G_VARIANT_TYPE ("a(ssa(ss)s)"), choices_string, NULL, NULL, &error); + g_assert_no_error (error); + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_string (keyfile, "backend", "choices", choices_string); + g_key_file_set_integer (keyfile, "result", "response", 2); + g_key_file_set_integer (keyfile, "result", "error_domain", XDG_DESKTOP_PORTAL_ERROR); + g_key_file_set_integer (keyfile, "result", "error_code", XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT); + + path = g_build_filename (outdir, "filechooser", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_open_file (portal, NULL, "test", NULL, NULL, choices, 0, NULL, open_file_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_open_file_parallel (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + const char * uris[] = { + "file:///test/file", + NULL + }; + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + g_key_file_set_string_list (keyfile, "result", "uris", uris, g_strv_length ((char **)uris)); + + path = g_build_filename (outdir, "filechooser", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_open_file (portal, NULL, "test", NULL, NULL, NULL, 0, NULL, open_file_cb, keyfile); + xdp_portal_open_file (portal, NULL, "test", NULL, NULL, NULL, 0, NULL, open_file_cb, keyfile); + xdp_portal_open_file (portal, NULL, "test", NULL, NULL, NULL, 0, NULL, open_file_cb, keyfile); + + while (got_info < 3) + g_main_context_iteration (NULL, TRUE); +} + +/* tests of SaveFile below */ + +static void +save_file_cb (GObject *obj, + GAsyncResult *result, + gpointer data) +{ + XdpPortal *portal = XDP_PORTAL (obj); + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) ret = NULL; + GKeyFile *keyfile = data; + int response; + int domain; + int code; + + response = g_key_file_get_integer (keyfile, "result", "response", NULL); + domain = g_key_file_get_integer (keyfile, "result", "error_domain", NULL); + code = g_key_file_get_integer (keyfile, "result", "error_code", NULL); + + ret = xdp_portal_save_file_finish (portal, result, &error); + if (response == 0) + { + const char * const *uris; + g_auto(GStrv) expected = NULL; + + g_assert_no_error (error); + g_variant_lookup (ret, "uris", "^a&s", &uris); + expected = g_key_file_get_string_list (keyfile, "result", "uris", NULL, NULL); + + g_assert (g_strv_equal (uris, (const char * const *)expected)); + } + else if (response == 1) + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED); + else if (response == 2) + g_assert_error (error, domain, code); + else + g_assert_not_reached (); + + got_info++; + + g_main_context_wakeup (NULL); +} + +void +test_save_file_basic (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + const char * uris[] = { + "file:///test/file", + NULL + }; + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + g_key_file_set_string_list (keyfile, "result", "uris", uris, g_strv_length ((char **)uris)); + + path = g_build_filename (outdir, "filechooser", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_save_file (portal, NULL, "test", "test_file.txt", NULL, NULL, NULL, NULL, NULL, 0, NULL, save_file_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_save_file_delay (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + const char * uris[] = { + "file:///test/file", + NULL + }; + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + g_key_file_set_string_list (keyfile, "result", "uris", uris, g_strv_length ((char **)uris)); + + path = g_build_filename (outdir, "filechooser", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_save_file (portal, NULL, "test", "test_file.txt", NULL, NULL, NULL, NULL, NULL, 0, NULL, save_file_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_save_file_cancel (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + const char * uris[] = { + "file:///test/file", + NULL + }; + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 1); + g_key_file_set_integer (keyfile, "result", "response", 1); + g_key_file_set_string_list (keyfile, "result", "uris", uris, g_strv_length ((char **)uris)); + + path = g_build_filename (outdir, "filechooser", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_save_file (portal, NULL, "test", "test_file.txt", NULL, NULL, NULL, NULL, NULL, 0, NULL, save_file_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_save_file_close (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GCancellable) cancellable = NULL; + g_autofree char *path = NULL; + const char * uris[] = { + "file:///test/file", + NULL + }; + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "expect-close", 1); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 1); + g_key_file_set_string_list (keyfile, "result", "uris", uris, g_strv_length ((char **)uris)); + + path = g_build_filename (outdir, "filechooser", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + cancellable = g_cancellable_new (); + + got_info = 0; + xdp_portal_save_file (portal, NULL, "test", "test_file.txt", NULL, NULL, NULL, NULL, NULL, 0, cancellable, save_file_cb, keyfile); + + g_timeout_add (100, cancel_call, cancellable); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_save_file_filters (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + const char * uris[] = { + "file:///test/file1", + NULL + }; + g_autoptr(GVariant) filters = NULL; + const char *filter_string = + "[('Images', [(0, '*ico'), (1, 'image/png')]), ('Text', [(0, '*.txt')])]"; + + filters = g_variant_parse (G_VARIANT_TYPE ("a(sa(us))"), filter_string, NULL, NULL, &error); + + keyfile = g_key_file_new (); + + g_assert_no_error (error); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_string (keyfile, "backend", "filters", filter_string); + g_key_file_set_integer (keyfile, "result", "response", 0); + g_key_file_set_string_list (keyfile, "result", "uris", uris, g_strv_length ((char **)uris)); + + path = g_build_filename (outdir, "filechooser", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_save_file (portal, NULL, "test", "test_file.txt", NULL, NULL, filters, NULL, NULL, 0, NULL, save_file_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_save_file_lockdown (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + const char * uris[] = { + "file:///test/file", + NULL + }; + + tests_set_property_sync (G_DBUS_PROXY (lockdown), + "org.freedesktop.impl.portal.Lockdown", + "disable-save-to-disk", + g_variant_new_boolean (TRUE), + &error); + g_assert_no_error (error); + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 2); + g_key_file_set_integer (keyfile, "result", "error_domain", XDG_DESKTOP_PORTAL_ERROR); + g_key_file_set_integer (keyfile, "result", "error_code", XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED); + g_key_file_set_string_list (keyfile, "result", "uris", uris, g_strv_length ((char **)uris)); + + path = g_build_filename (outdir, "filechooser", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_save_file (portal, NULL, "test", "test_file.txt", NULL, NULL, NULL, NULL, NULL, 0, NULL, save_file_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); + + tests_set_property_sync (G_DBUS_PROXY (lockdown), + "org.freedesktop.impl.portal.Lockdown", + "disable-save-to-disk", + g_variant_new_boolean (FALSE), + &error); + g_assert_no_error (error); +} + +void +test_save_file_parallel (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + const char * uris[] = { + "file:///test/file", + NULL + }; + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + g_key_file_set_string_list (keyfile, "result", "uris", uris, g_strv_length ((char **)uris)); + + path = g_build_filename (outdir, "filechooser", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_save_file (portal, NULL, "test", "test_file.txt", NULL, NULL, NULL, NULL, NULL, 0, NULL, save_file_cb, keyfile); + xdp_portal_save_file (portal, NULL, "test", "test_file.txt", NULL, NULL, NULL, NULL, NULL, 0, NULL, save_file_cb, keyfile); + xdp_portal_save_file (portal, NULL, "test", "test_file.txt", NULL, NULL, NULL, NULL, NULL, 0, NULL, save_file_cb, keyfile); + + while (got_info < 3) + g_main_context_iteration (NULL, TRUE); +} diff --git a/tests/filechooser.h b/tests/filechooser.h new file mode 100644 index 0000000..523e4a9 --- /dev/null +++ b/tests/filechooser.h @@ -0,0 +1,25 @@ +#pragma once + +void test_open_file_basic (void); +void test_open_file_delay (void); +void test_open_file_cancel (void); +void test_open_file_close (void); +void test_open_file_multiple (void); +void test_open_file_filters1 (void); +void test_open_file_filters2 (void); +void test_open_file_current_filter1 (void); +void test_open_file_current_filter2 (void); +void test_open_file_current_filter3 (void); +void test_open_file_current_filter4 (void); +void test_open_file_choices1 (void); +void test_open_file_choices2 (void); +void test_open_file_choices3 (void); +void test_open_file_parallel (void); + +void test_save_file_basic (void); +void test_save_file_delay (void); +void test_save_file_cancel (void); +void test_save_file_close (void); +void test_save_file_filters (void); +void test_save_file_lockdown (void); +void test_save_file_parallel (void); diff --git a/tests/glib-backports.c b/tests/glib-backports.c new file mode 100644 index 0000000..256dc37 --- /dev/null +++ b/tests/glib-backports.c @@ -0,0 +1,24 @@ +#include "glib-backports.h" + +#if !GLIB_CHECK_VERSION(2,60,0) + +gboolean +g_strv_equal (const gchar * const *strv1, + const gchar * const *strv2) +{ + g_return_val_if_fail (strv1 != NULL, FALSE); + g_return_val_if_fail (strv2 != NULL, FALSE); + + if (strv1 == strv2) + return TRUE; + + for (; *strv1 != NULL && *strv2 != NULL; strv1++, strv2++) + { + if (!g_str_equal (*strv1, *strv2)) + return FALSE; + } + + return (*strv1 == NULL && *strv2 == NULL); +} + +#endif diff --git a/tests/glib-backports.h b/tests/glib-backports.h new file mode 100644 index 0000000..62d317b --- /dev/null +++ b/tests/glib-backports.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +#if !GLIB_CHECK_VERSION(2,60,0) +gboolean g_strv_equal (const gchar * const *strv1, + const gchar * const *strv2); +#endif diff --git a/tests/inhibit.c b/tests/inhibit.c new file mode 100644 index 0000000..473c964 --- /dev/null +++ b/tests/inhibit.c @@ -0,0 +1,404 @@ +#include + +#include "inhibit.h" + +#include +#include "xdp-impl-dbus.h" + +extern char outdir[]; + +extern XdpDbusImplPermissionStore *permission_store; + +static void +set_inhibit_permissions (const char **permissions) +{ + g_autoptr(GError) error = NULL; + + xdp_dbus_impl_permission_store_call_set_permission_sync (permission_store, + "inhibit", + TRUE, + "inhibit", + "", + permissions, + NULL, + &error); + g_assert_no_error (error); +} + +static void +unset_inhibit_permissions (void) +{ + xdp_dbus_impl_permission_store_call_delete_sync (permission_store, + "inhibit", + "inhibit", + NULL, + NULL); + /* Ignore the error here, since this fails if the table doesn't exist */ +} + +static int got_info; +static int inhibit_id[3]; + +static void +inhibit_cb (GObject *object, + GAsyncResult *result, + gpointer data) +{ + XdpPortal *portal = XDP_PORTAL (object); + GKeyFile *keyfile = data; + g_autoptr(GError) error = NULL; + int response; + int id; + + g_debug ("Got inhibit callback"); + + response = g_key_file_get_integer (keyfile, "result", "response", NULL); + + id = xdp_portal_session_inhibit_finish (portal, result, &error); + + if (response == 0) + { + g_assert_no_error (error); + g_assert_cmpint (id, >, 0); + } + else + { + g_assert_nonnull (error); + g_assert_cmpint (id, ==, -1); + } + + g_assert (0 <= got_info && got_info < 3); + inhibit_id[got_info] = id; + + got_info++; + g_main_context_wakeup (NULL); +} + +void +test_inhibit_basic (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + XdpInhibitFlags flags; + const char *perms[] = { "logout", "suspend", NULL }; + + set_inhibit_permissions (perms); + unset_inhibit_permissions (); + + keyfile = g_key_file_new (); + + flags = XDP_INHIBIT_FLAG_LOGOUT|XDP_INHIBIT_FLAG_USER_SWITCH; + + g_key_file_set_integer (keyfile, "inhibit", "flags", flags); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "inhibit", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_session_inhibit (portal, NULL, "Testing portals", flags, NULL, inhibit_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); + + xdp_portal_session_uninhibit (portal, inhibit_id[0]); +} + +void +test_inhibit_delay (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + XdpInhibitFlags flags; + + unset_inhibit_permissions (); + + keyfile = g_key_file_new (); + + flags = XDP_INHIBIT_FLAG_USER_SWITCH|XDP_INHIBIT_FLAG_IDLE; + + g_key_file_set_integer (keyfile, "inhibit", "flags", flags); + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "inhibit", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_session_inhibit (portal, NULL, "Testing portals", flags, NULL, inhibit_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); + + xdp_portal_session_uninhibit (portal, inhibit_id[0]); +} + +void +test_inhibit_cancel (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + XdpInhibitFlags flags; + + unset_inhibit_permissions (); + + keyfile = g_key_file_new (); + + flags = XDP_INHIBIT_FLAG_USER_SWITCH|XDP_INHIBIT_FLAG_IDLE; + + g_key_file_set_integer (keyfile, "inhibit", "flags", flags); + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 1); + g_key_file_set_integer (keyfile, "result", "response", 1); + + path = g_build_filename (outdir, "inhibit", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_session_inhibit (portal, NULL, "Testing portals", flags, NULL, inhibit_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +static gboolean +cancel_call (gpointer data) +{ + GCancellable *cancellable = data; + + g_debug ("cancel call"); + g_cancellable_cancel (cancellable); + + return G_SOURCE_REMOVE; +} + +void +test_inhibit_close (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + XdpInhibitFlags flags; + g_autoptr(GCancellable) cancellable = NULL; + + unset_inhibit_permissions (); + + keyfile = g_key_file_new (); + + flags = XDP_INHIBIT_FLAG_USER_SWITCH|XDP_INHIBIT_FLAG_IDLE; + + g_key_file_set_integer (keyfile, "inhibit", "flags", flags); + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 1); + g_key_file_set_integer (keyfile, "result", "response", 1); + g_key_file_set_boolean (keyfile, "backend", "expect-close", 1); + + path = g_build_filename (outdir, "inhibit", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + cancellable = g_cancellable_new (); + + got_info = 0; + xdp_portal_session_inhibit (portal, NULL, "Testing portals", flags, cancellable, inhibit_cb, keyfile); + + g_timeout_add (100, cancel_call, cancellable); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_inhibit_permissions (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + XdpInhibitFlags flags; + const char *permissions[] = { "logout", "suspend", NULL }; + + set_inhibit_permissions (permissions); + + keyfile = g_key_file_new (); + + flags = XDP_INHIBIT_FLAG_LOGOUT|XDP_INHIBIT_FLAG_USER_SWITCH; + + g_key_file_set_integer (keyfile, "inhibit", "flags", XDP_INHIBIT_FLAG_LOGOUT); /* user switch is not allowed */ + + path = g_build_filename (outdir, "inhibit", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_session_inhibit (portal, NULL, "Testing portals", flags, NULL, inhibit_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); + + xdp_portal_session_uninhibit (portal, inhibit_id[0]); + + unset_inhibit_permissions (); +} + +void +test_inhibit_parallel (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + XdpInhibitFlags flags; + + unset_inhibit_permissions (); + + keyfile = g_key_file_new (); + + flags = XDP_INHIBIT_FLAG_USER_SWITCH|XDP_INHIBIT_FLAG_IDLE; + + g_key_file_set_integer (keyfile, "inhibit", "flags", flags); + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "inhibit", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_session_inhibit (portal, NULL, "Testing portals", flags, NULL, inhibit_cb, keyfile); + xdp_portal_session_inhibit (portal, NULL, "Testing portals", flags, NULL, inhibit_cb, keyfile); + xdp_portal_session_inhibit (portal, NULL, "Testing portals", flags, NULL, inhibit_cb, keyfile); + + while (got_info < 3) + g_main_context_iteration (NULL, TRUE); + + xdp_portal_session_uninhibit (portal, inhibit_id[0]); + xdp_portal_session_uninhibit (portal, inhibit_id[1]); + xdp_portal_session_uninhibit (portal, inhibit_id[2]); +} + +/* tests below test session state monitoring */ + +static void +monitor_cb (GObject *object, + GAsyncResult *result, + gpointer data) +{ + XdpPortal *portal = XDP_PORTAL (object); + g_autoptr(GError) error = NULL; + gboolean ret; + + ret = xdp_portal_session_monitor_start_finish (portal, result, &error); + g_assert_true (ret); + g_assert_no_error (error); + + got_info += 1; +} + +static void +session_state_changed_cb (XdpPortal *portal, + gboolean screensaver_active, + XdpLoginSessionState state, + gpointer data) +{ + g_assert_false (screensaver_active); + g_assert_cmpint (state, ==, XDP_LOGIN_SESSION_RUNNING); + + got_info += 1; +} + +static void +session_state_changed_cb2 (XdpPortal *portal, + gboolean screensaver_active, + XdpLoginSessionState state, + gpointer data) +{ + g_assert_false (screensaver_active); + g_assert_cmpint (state, ==, XDP_LOGIN_SESSION_QUERY_END); + + got_info += 1; +} + +static gboolean +bump_got_info (gpointer data) +{ + got_info += 1; + + return G_SOURCE_REMOVE; +} + +void +test_inhibit_monitor (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + gulong id; + + if (g_getenv ("TEST_IN_CI")) + { + g_test_skip ("Skip tests that are unreliable in CI"); + return; + } + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 1000); + g_key_file_set_string (keyfile, "backend", "change", "query-end"); + + path = g_build_filename (outdir, "inhibit", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + id = g_signal_connect (portal, "session-state-changed", G_CALLBACK (session_state_changed_cb), NULL); + + got_info = 0; + xdp_portal_session_monitor_start (portal, NULL, 0, NULL, monitor_cb, NULL); + + /* we get a monitor_cb and an initial state-changed emission */ + while (got_info < 2) + g_main_context_iteration (NULL, TRUE); + + g_signal_handler_disconnect (portal, id); + + /* now wait for the query-end state */ + g_debug ("waiting for query-end state\n"); + got_info = 0; + g_signal_connect (portal, "session-state-changed", G_CALLBACK (session_state_changed_cb2), NULL); + + while (got_info < 1) + g_main_context_iteration (NULL, TRUE); + + xdp_portal_session_monitor_stop (portal); + + /* after calling stop, no more state-changed signals */ + got_info = 0; + g_timeout_add (500, bump_got_info, NULL); + while (got_info < 1) + g_main_context_iteration (NULL, TRUE); +} diff --git a/tests/inhibit.h b/tests/inhibit.h new file mode 100644 index 0000000..fca8292 --- /dev/null +++ b/tests/inhibit.h @@ -0,0 +1,10 @@ +#pragma once + +void test_inhibit_basic (void); +void test_inhibit_delay (void); +void test_inhibit_close (void); +void test_inhibit_cancel (void); +void test_inhibit_parallel (void); +void test_inhibit_permissions (void); + +void test_inhibit_monitor (void); diff --git a/tests/limited-portals.c b/tests/limited-portals.c new file mode 100644 index 0000000..9c96f5f --- /dev/null +++ b/tests/limited-portals.c @@ -0,0 +1,492 @@ +#include "config.h" + +#include +#include + +#include + +#include "src/glib-backports.h" +#include "xdp-dbus.h" +#include "xdp-utils.h" +#include "xdp-impl-dbus.h" + +#ifdef HAVE_LIBPORTAL +#include "account.h" +#include "background.h" +#include "camera.h" +#include "email.h" +#include "filechooser.h" +#include "inhibit.h" +#include "location.h" +#include "notification.h" +#include "openuri.h" +#include "print.h" +#include "screenshot.h" +#include "trash.h" +#include "wallpaper.h" +#endif + +#include "utils.h" + +/* required while we support meson + autotools. Autotools builds everything in + the root dir ('.'), meson builds in each subdir nested and overrides these for + g_test_build_filename */ +#ifndef XDG_DP_BUILDDIR +#define XDG_DP_BUILDDIR "." +#endif +#ifndef XDG_PS_BUILDDIR +#define XDG_PS_BUILDDIR "." +#endif + +#define PORTAL_BUS_NAME "org.freedesktop.portal.Desktop" +#define PORTAL_OBJECT_PATH "/org/freedesktop/portal/desktop" +#define BACKEND_BUS_NAME "org.freedesktop.impl.portal.Limited" +#define BACKEND_OBJECT_PATH "/org/freedesktop/portal/desktop" + +#include "document-portal/permission-store-dbus.h" + +char outdir[] = "/tmp/xdp-test-XXXXXX"; + +static GTestDBus *dbus; +static GDBusConnection *session_bus; +static GList *test_procs = NULL; +XdpDbusImplPermissionStore *permission_store; +XdpDbusImplLockdown *lockdown; + +int +xdup (int oldfd) +{ + int newfd = dup (oldfd); + + if (newfd < 0) + g_error ("Unable to duplicate fd %d: %s", oldfd, g_strerror (errno)); + + return newfd; +} + +static void +name_appeared_cb (GDBusConnection *bus, + const char *name, + const char *name_owner, + gpointer data) +{ + gboolean *b = (gboolean *)data; + + g_debug ("Name %s now owned by %s\n", name, name_owner); + + *b = TRUE; + + g_main_context_wakeup (NULL); +} + +static void +name_disappeared_cb (GDBusConnection *bus, + const char *name, + gpointer data) +{ + g_debug ("Name %s disappeared\n", name); +} + +static gboolean +timeout_cb (gpointer data) +{ + const char *msg = data; + + g_error ("%s", msg); + + return G_SOURCE_REMOVE; +} + +static void +update_data_dirs (void) +{ + const char *data_dirs; + gssize len = 0; + GString *str; + char *new_val; + + data_dirs = g_getenv ("XDG_DATA_DIRS"); + if (data_dirs != NULL && + strstr (data_dirs, "/usr/share") != NULL) + { + return; + } + + if (data_dirs != NULL) + { + len = strlen (data_dirs); + if (data_dirs[len] == ':') + len--; + } + + str = g_string_new_len (data_dirs, len); + if (str->len > 0) + g_string_append_c (str, ':'); + g_string_append (str, "/usr/local/share/:/usr/share/"); + new_val = g_string_free (str, FALSE); + + g_debug ("Setting XDG_DATA_DIRS to %s", new_val); + g_setenv ("XDG_DATA_DIRS", new_val, TRUE); + /* new_val is leaked */ +} + +static void +global_setup (void) +{ + GError *error = NULL; + g_autofree gchar *backends_executable = NULL; + g_autofree gchar *services = NULL; + g_autofree gchar *portal_dir = NULL; + g_autofree gchar *argv0 = NULL; + g_autoptr(GSubprocessLauncher) launcher = NULL; + g_autoptr(GSubprocess) subprocess = NULL; + guint name_timeout; + const char *argv[4]; + GQuark portal_errors G_GNUC_UNUSED; + static gboolean name_appeared; + guint watch; + guint timeout_mult = 1; + + update_data_dirs (); + + g_mkdtemp (outdir); + g_debug ("outdir: %s\n", outdir); + + g_setenv ("XDG_CURRENT_DESKTOP", "limited", TRUE); + g_setenv ("XDG_RUNTIME_DIR", outdir, TRUE); + g_setenv ("XDG_DATA_HOME", outdir, TRUE); + + /* Re-defining dbus-daemon with a custom script */ + setup_dbus_daemon_wrapper (outdir); + + dbus = g_test_dbus_new (G_TEST_DBUS_NONE); + services = g_test_build_filename (G_TEST_BUILT, "services", NULL); + g_test_dbus_add_service_dir (dbus, services); + g_test_dbus_up (dbus); + + if (g_getenv ("TEST_IN_CI")) + timeout_mult = 10; + + /* g_test_dbus_up unsets this, so re-set */ + g_setenv ("XDG_RUNTIME_DIR", outdir, TRUE); + + session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); + g_assert_no_error (error); + + /* start portal backends */ + name_appeared = FALSE; + watch = g_bus_watch_name_on_connection (session_bus, + BACKEND_BUS_NAME, + 0, + name_appeared_cb, + name_disappeared_cb, + &name_appeared, + NULL); + + launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_NONE); + g_subprocess_launcher_setenv (launcher, "G_DEBUG", "fatal-criticals", TRUE); + g_subprocess_launcher_setenv (launcher, "DBUS_SESSION_BUS_ADDRESS", g_test_dbus_get_bus_address (dbus), TRUE); + g_subprocess_launcher_setenv (launcher, "XDG_DATA_HOME", outdir, TRUE); + g_subprocess_launcher_setenv (launcher, "PATH", g_getenv ("PATH"), TRUE); + g_subprocess_launcher_take_stdout_fd (launcher, xdup (STDERR_FILENO)); + + backends_executable = g_test_build_filename (G_TEST_BUILT, "test-backends", NULL); + argv[0] = backends_executable; + argv[1] = "--backend-name=" BACKEND_BUS_NAME; + argv[2] = g_test_verbose () ? "--verbose" : NULL; + argv[3] = NULL; + + g_debug ("launching test-backend\n"); + + subprocess = g_subprocess_launcher_spawnv (launcher, argv, &error); + g_assert_no_error (error); + g_test_message ("Launched %s with pid %s\n", argv[0], + g_subprocess_get_identifier (subprocess)); + test_procs = g_list_append (test_procs, g_steal_pointer (&subprocess)); + + name_timeout = g_timeout_add (1000 * timeout_mult, timeout_cb, "Failed to launch test-backends"); + + while (!name_appeared) + g_main_context_iteration (NULL, TRUE); + + g_source_remove (name_timeout); + g_bus_unwatch_name (watch); + + /* start permission store */ + name_appeared = FALSE; + watch = g_bus_watch_name_on_connection (session_bus, + "org.freedesktop.impl.portal.PermissionStore", + 0, + name_appeared_cb, + name_disappeared_cb, + &name_appeared, + NULL); + + g_clear_object (&launcher); + launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_NONE); + g_subprocess_launcher_setenv (launcher, "G_DEBUG", "fatal-criticals", TRUE); + g_subprocess_launcher_setenv (launcher, "DBUS_SESSION_BUS_ADDRESS", g_test_dbus_get_bus_address (dbus), TRUE); + g_subprocess_launcher_setenv (launcher, "XDG_DATA_HOME", outdir, TRUE); + g_subprocess_launcher_setenv (launcher, "PATH", g_getenv ("PATH"), TRUE); + g_subprocess_launcher_take_stdout_fd (launcher, xdup (STDERR_FILENO)); + + if (g_getenv ("XDP_UNINSTALLED") != NULL) + argv0 = g_test_build_filename (G_TEST_BUILT, "..", XDG_PS_BUILDDIR, "xdg-permission-store", NULL); + else + argv0 = g_strdup (LIBEXECDIR "/xdg-permission-store"); + + argv[0] = argv0; + argv[1] = "--replace"; + argv[2] = g_test_verbose () ? "--verbose" : NULL; + argv[3] = NULL; + + g_debug ("launching %s\n", argv0); + + subprocess = g_subprocess_launcher_spawnv (launcher, argv, &error); + g_assert_no_error (error); + g_test_message ("Launched %s with pid %s\n", argv[0], + g_subprocess_get_identifier (subprocess)); + test_procs = g_list_append (test_procs, g_steal_pointer (&subprocess)); + + name_timeout = g_timeout_add (1000 * timeout_mult, timeout_cb, "Failed to launch xdg-permission-store"); + + while (!name_appeared) + g_main_context_iteration (NULL, TRUE); + + g_source_remove (name_timeout); + g_bus_unwatch_name (watch); + + /* start portals */ + name_appeared = FALSE; + watch = g_bus_watch_name_on_connection (session_bus, + PORTAL_BUS_NAME, + 0, + name_appeared_cb, + name_disappeared_cb, + &name_appeared, + NULL); + + portal_dir = g_test_build_filename (G_TEST_BUILT, "portals", "limited", NULL); + + g_clear_object (&launcher); + launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_NONE); + g_subprocess_launcher_setenv (launcher, "G_DEBUG", "fatal-criticals", TRUE); + g_subprocess_launcher_setenv (launcher, "DBUS_SESSION_BUS_ADDRESS", g_test_dbus_get_bus_address (dbus), TRUE); + g_subprocess_launcher_setenv (launcher, "XDG_DESKTOP_PORTAL_DIR", portal_dir, TRUE); + g_subprocess_launcher_setenv (launcher, "XDG_DATA_HOME", outdir, TRUE); + g_subprocess_launcher_setenv (launcher, "PATH", g_getenv ("PATH"), TRUE); + g_subprocess_launcher_take_stdout_fd (launcher, xdup (STDERR_FILENO)); + + if (g_getenv ("XDP_UNINSTALLED") != NULL) + argv0 = g_test_build_filename (G_TEST_BUILT, "..", XDG_DP_BUILDDIR, "xdg-desktop-portal", NULL); + else + argv0 = g_strdup (LIBEXECDIR "/xdg-desktop-portal"); + + argv[0] = argv0; + argv[1] = g_test_verbose () ? "--verbose" : NULL; + argv[2] = NULL; + + g_debug ("launching %s\n", argv0); + + subprocess = g_subprocess_launcher_spawnv (launcher, argv, &error); + g_assert_no_error (error); + g_test_message ("Launched %s with pid %s\n", argv[0], + g_subprocess_get_identifier (subprocess)); + test_procs = g_list_append (test_procs, g_steal_pointer (&subprocess)); + g_clear_pointer (&argv0, g_free); + + name_timeout = g_timeout_add (1000 * timeout_mult, timeout_cb, "Failed to launch xdg-desktop-portal"); + + while (!name_appeared) + g_main_context_iteration (NULL, TRUE); + + g_source_remove (name_timeout); + g_bus_unwatch_name (watch); + + permission_store = xdp_dbus_impl_permission_store_proxy_new_sync (session_bus, + 0, + "org.freedesktop.impl.portal.PermissionStore", + "/org/freedesktop/impl/portal/PermissionStore", + NULL, + &error); + g_assert_no_error (error); + + lockdown = xdp_dbus_impl_lockdown_proxy_new_sync (session_bus, + 0, + BACKEND_BUS_NAME, + BACKEND_OBJECT_PATH, + NULL, + &error); + g_assert_no_error (error); + + /* make sure errors are registered */ + portal_errors = XDG_DESKTOP_PORTAL_ERROR; +} + +static void +wait_for_test_procs (void) +{ + GList *l; + + for (l = test_procs; l; l = l->next) + { + GSubprocess *subprocess = G_SUBPROCESS (l->data); + GError *error = NULL; + g_autofree char *identifier = NULL; + + identifier = g_strdup (g_subprocess_get_identifier (subprocess)); + + g_debug ("Terminating and waiting for process %s", identifier); + g_subprocess_send_signal (subprocess, SIGTERM); + + /* This may lead the test to hang, we assume that the test suite or CI + * can handle the case at upper level, without having us async function + * and timeouts */ + g_subprocess_wait (subprocess, NULL, &error); + g_assert_no_error (error); + g_assert_null (g_subprocess_get_identifier (subprocess)); + + if (!g_subprocess_get_if_exited (subprocess)) + { + g_assert_true (g_subprocess_get_if_signaled (subprocess)); + g_assert_cmpint (g_subprocess_get_term_sig (subprocess), ==, SIGTERM); + } + else if (!g_subprocess_get_successful (subprocess)) + { + g_error ("Subprocess %s, exited with exit status %d", identifier, + g_subprocess_get_exit_status (subprocess)); + } + } +} + +static void +global_teardown (void) +{ + GError *error = NULL; + + g_dbus_connection_flush_sync (session_bus, NULL, &error); + g_assert_no_error (error); + + g_dbus_connection_close_sync (session_bus, NULL, &error); + g_assert_no_error (error); + + wait_for_test_procs (); + g_list_free_full (g_steal_pointer (&test_procs), g_object_unref); + + g_object_unref (lockdown); + g_object_unref (permission_store); + + g_object_unref (session_bus); + + g_test_dbus_down (dbus); + + g_object_unref (dbus); +} + +/* Just check that the portal is there, and has the + * expected version. This will fail if the backend + * is not found. + */ +#define DEFINE_TEST_EXISTS(pp,PP,version) \ +static void \ +test_##pp##_exists (void) \ +{ \ + g_autoptr(GDBusProxy) proxy = NULL; \ + g_autoptr(GError) error = NULL; \ + g_autofree char *owner = NULL; \ + \ + proxy = G_DBUS_PROXY (xdp_dbus_##pp##_proxy_new_sync (session_bus, \ + 0, \ + PORTAL_BUS_NAME, \ + PORTAL_OBJECT_PATH, \ + NULL, \ + &error)); \ + g_assert_no_error (error); \ + \ + owner = g_dbus_proxy_get_name_owner (proxy); \ + g_assert_nonnull (owner); \ + \ + g_assert_cmpuint (xdp_dbus_##pp##_get_version (XDP_DBUS_##PP (proxy)), ==, version); \ +} + +/* Just check that the portal is not there. + * + * We do a version check, but we hardcode the default value of zero, + * as all portals will have a version greater than, or equal to one. + */ +#define DEFINE_TEST_DOES_NOT_EXIST(pp,PP) \ +static void \ +test_##pp##_does_not_exist (void) \ +{ \ + g_autoptr(GDBusProxy) proxy = NULL; \ + g_autoptr(GError) error = NULL; \ + g_autofree char *owner = NULL; \ + \ + proxy = G_DBUS_PROXY (xdp_dbus_##pp##_proxy_new_sync (session_bus, \ + 0, \ + PORTAL_BUS_NAME, \ + PORTAL_OBJECT_PATH, \ + NULL, \ + &error)); \ + g_assert_no_error (error); \ + \ + owner = g_dbus_proxy_get_name_owner (proxy); \ + g_assert_nonnull (owner); \ + \ + g_assert_cmpuint (xdp_dbus_##pp##_get_version (XDP_DBUS_##PP (proxy)), ==, 0); \ +} + +DEFINE_TEST_EXISTS(file_chooser, FILE_CHOOSER, 3) + +DEFINE_TEST_DOES_NOT_EXIST(print, PRINT) + +int +main (int argc, char **argv) +{ + int res; + + g_log_writer_default_set_use_stderr (TRUE); + + setlocale (LC_ALL, NULL); + + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/limited/filechooser/exists", test_file_chooser_exists); + g_test_add_func ("/limited/print/does-not-exist", test_print_does_not_exist); + +#ifdef HAVE_LIBPORTAL + g_test_add_func ("/limited/openfile/basic", test_open_file_basic); + g_test_add_func ("/limited/openfile/delay", test_open_file_delay); + g_test_add_func ("/limited/openfile/close", test_open_file_close); + g_test_add_func ("/limited/openfile/cancel", test_open_file_cancel); + g_test_add_func ("/limited/openfile/multiple", test_open_file_multiple); + g_test_add_func ("/limited/openfile/filters1", test_open_file_filters1); + g_test_add_func ("/limited/openfile/filters2", test_open_file_filters2); + g_test_add_func ("/limited/openfile/current_filter1", test_open_file_current_filter1); + g_test_add_func ("/limited/openfile/current_filter2", test_open_file_current_filter2); + g_test_add_func ("/limited/openfile/current_filter3", test_open_file_current_filter3); + g_test_add_func ("/limited/openfile/current_filter4", test_open_file_current_filter4); + g_test_add_func ("/limited/openfile/choices1", test_open_file_choices1); + g_test_add_func ("/limited/openfile/choices2", test_open_file_choices2); + g_test_add_func ("/limited/openfile/choices3", test_open_file_choices3); + g_test_add_func ("/limited/openfile/parallel", test_open_file_parallel); + + g_test_add_func ("/limited/savefile/basic", test_save_file_basic); + g_test_add_func ("/limited/savefile/delay", test_save_file_delay); + g_test_add_func ("/limited/savefile/close", test_save_file_close); + g_test_add_func ("/limited/savefile/cancel", test_save_file_cancel); + g_test_add_func ("/limited/savefile/filters", test_save_file_filters); + g_test_add_func ("/limited/savefile/lockdown", test_save_file_lockdown); + g_test_add_func ("/limited/savefile/parallel", test_save_file_parallel); +#endif + + global_setup (); + + res = g_test_run (); + + sleep (1); + + global_teardown (); + + return res; +} diff --git a/tests/location.c b/tests/location.c new file mode 100644 index 0000000..8726bc6 --- /dev/null +++ b/tests/location.c @@ -0,0 +1,107 @@ +#include + +#include "location.h" + +#include "xdp-utils.h" +#include + +extern char outdir[]; + +static int got_info = 0; + +static void +location_cb (GObject *source, + GAsyncResult *result, + gpointer data) +{ + XdpPortal *portal = XDP_PORTAL (source); + g_autoptr(GError) error = NULL; + gboolean res; + + res = xdp_portal_location_monitor_start_finish (portal, result, &error); + g_assert_true (res); + g_assert_no_error (error); + + got_info = 1; +} + +void +test_location_basic (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GDBusConnection) system_bus = NULL; + g_autoptr(GError) error = NULL; + +#ifndef HAVE_GEOCLUE + g_test_skip ("Skipping tests that require geoclue"); + return; +#endif + + system_bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &error); + + if (system_bus == NULL) + { + g_prefix_error (&error, "Unable to test Location without system bus: "); + g_test_skip (error->message); + return; + } + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_location_monitor_start (portal, NULL, 0, 0, XDP_LOCATION_ACCURACY_EXACT, 0, NULL, location_cb, NULL); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); + + xdp_portal_location_monitor_stop (portal); +} + +static void +location_error (GObject *source, + GAsyncResult *result, + gpointer data) +{ + XdpPortal *portal = XDP_PORTAL (source); + g_autoptr(GError) error = NULL; + gboolean res; + + res = xdp_portal_location_monitor_start_finish (portal, result, &error); + g_assert_false (res); + g_assert_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT); + + got_info = 1; +} + +void +test_location_accuracy (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GDBusConnection) system_bus = NULL; + g_autoptr(GError) error = NULL; + +#ifndef HAVE_GEOCLUE + g_test_skip ("Skipping tests that require geoclue"); + return; +#endif + + system_bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &error); + + if (system_bus == NULL) + { + g_prefix_error (&error, "Unable to test Location without system bus: "); + g_test_skip (error->message); + return; + } + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_location_monitor_start (portal, NULL, 0, 0, 22, 0, NULL, location_error, NULL); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); + + xdp_portal_location_monitor_stop (portal); +} + diff --git a/tests/location.h b/tests/location.h new file mode 100644 index 0000000..0f089f5 --- /dev/null +++ b/tests/location.h @@ -0,0 +1,4 @@ +#pragma once + +void test_location_basic (void); +void test_location_accuracy (void); diff --git a/tests/meson.build b/tests/meson.build new file mode 100644 index 0000000..b91800f --- /dev/null +++ b/tests/meson.build @@ -0,0 +1,373 @@ +env_tests = environment() +env_tests.set('XDP_UNINSTALLED', '1') +env_tests.set('XDG_DATA_DIRS', meson.current_build_dir() / 'share') +env_tests.set('G_TEST_SRCDIR', meson.current_source_dir()) +env_tests.set('G_TEST_BUILDDIR', meson.current_build_dir()) +env_tests.set('G_DEBUG', 'gc-friendly') # from glib-tap.mk + +if glib_dep.version().version_compare('>= 2.68') + test_protocol = 'tap' +else + test_protocol = 'exitcode' +endif + +subdir('dbs') +subdir('portals') +subdir('services') +subdir('share') + +test_db = executable( + 'testdb', + ['testdb.c'] + db_sources, + dependencies: [common_deps], + include_directories: [common_includes], + install: enable_installed_tests, + install_dir: installed_tests_dir, +) +test( + 'testdb', + test_db, + env: env_tests, + is_parallel: false, + protocol: test_protocol, +) + +if enable_installed_tests + install_data('dbs/no_tables', install_dir: installed_tests_dir / 'dbs') +endif + +test_doc_portal = executable( + 'test-doc-portal', + 'can-use-fuse.c', + 'test-doc-portal.c', + 'utils.c', + document_portal_built_sources, + dependencies: [common_deps, fuse3_dep], + include_directories: [common_includes], + install: enable_installed_tests, + install_dir: installed_tests_dir, +) +test( + 'test-doc-portal', + test_doc_portal, + env: env_tests, + is_parallel: false, + protocol: test_protocol, +) + +test_backends_sources = files( + 'backend/test-backends.c', + 'backend/request.c', + 'backend/session.c', + 'backend/access.c', + 'backend/account.c', + 'backend/appchooser.c', + 'backend/background.c', + 'backend/email.c', + 'backend/filechooser.c', + 'backend/inhibit.c', + 'backend/lockdown.c', + 'backend/notification.c', + 'backend/print.c', + 'backend/screenshot.c', + 'backend/settings.c', + 'backend/wallpaper.c', + 'glib-backports.c', +) + +# We build this in the tests/ subdirectory so that it can be accessed +# via G_TEST_BUILT +test_backends = executable( + 'test-backends', + test_backends_sources, + document_portal_built_sources, + document_portal_built_sources, + impl_built_sources, + dependencies: [common_deps], + include_directories: [common_includes, src_includes], + install: enable_installed_tests, + install_dir: installed_tests_dir, +) + +test_portals_sources = files( + 'test-portals.c', + 'utils.c', +) + +limited_portals_sources = files( + 'limited-portals.c', + 'utils.c', +) + +if have_libportal + extra_portals_sources = files( + 'account.c', + 'background.c', + 'camera.c', + 'email.c', + 'filechooser.c', + 'inhibit.c', + 'location.c', + 'notification.c', + 'openuri.c', + 'print.c', + 'screenshot.c', + 'trash.c', + 'wallpaper.c', + 'glib-backports.c', + ) + + test_portals_sources += extra_portals_sources + limited_portals_sources += extra_portals_sources +endif + +test_portals = executable( + 'test-portals', + impl_built_sources, + permission_store_built_sources, + portal_built_sources, + sd_escape_sources, + test_portals_sources, + xdp_utils_sources, + dependencies: [common_deps, libportal_dep, libsystemd_dep], + include_directories: [common_includes, xdp_utils_includes], + c_args: [ + '-DXDG_DP_BUILDDIR="src"', + '-DXDG_PS_BUILDDIR="document-portal"', + ], + install: enable_installed_tests, + install_dir: installed_tests_dir, +) + +limited_portals = executable( + 'limited-portals', + impl_built_sources, + permission_store_built_sources, + portal_built_sources, + sd_escape_sources, + limited_portals_sources, + xdp_utils_sources, + dependencies: [common_deps, libportal_dep, libsystemd_dep], + include_directories: [common_includes, xdp_utils_includes], + c_args: [ + '-DXDG_DP_BUILDDIR="src"', + '-DXDG_PS_BUILDDIR="document-portal"', + ], + install: enable_installed_tests, + install_dir: installed_tests_dir, +) + +# Split the portal tests into one test per portal, this makes debugging a lot +# easier. +# Keep in sync with test-portals.c +portal_tests = [ + 'account', + 'background', + 'camera', + 'color', + 'email', + 'inhibit', + 'location', + 'notification', + 'openfile', + 'openuri', + 'prepareprint', + 'print', + 'savefile', + 'screenshot', + 'trash', + 'wallpaper', +] + +test_env = env_tests +test_env.set('XDG_CURRENT_DESKTOP', 'test') +foreach p : portal_tests + test( + 'test-portals-@0@'.format(p), + test_portals, + args: ['--verbose', '--keep-going', '--tap', '-p', '/portal/@0@'.format(p)], + depends: [test_backends, test_portals], + env: test_env, + is_parallel: false, + protocol: test_protocol, + suite: 'portals', + ) +endforeach + +# Split the portal tests into one test per portal, this makes debugging a lot +# easier. +# Keep in sync with test-portals.c +portal_limited = [ + 'openfile', + 'savefile', +] + +limited_env = env_tests +limited_env.set('XDG_CURRENT_DESKTOP', 'limited') +foreach p : portal_limited + test( + 'limited-portals-@0@'.format(p), + limited_portals, + args: ['--verbose', '--keep-going', '--tap', '-p', '/limited/@0@'.format(p)], + depends: [test_backends, limited_portals], + env: limited_env, + is_parallel: false, + protocol: test_protocol, + suite: 'portals', + ) +endforeach + +if enable_installed_tests + install_data( + 'session.conf.in', + 'test-document-fuse.sh', + 'test-document-fuse.py', + install_dir: installed_tests_dir + ) +endif + +test_permission_store = executable( + 'test-permission-store', + 'test-permission-store.c', + 'utils.c', + permission_store_built_sources, + xdp_utils_sources, + sd_escape_sources, + dependencies: [common_deps, libsystemd_dep], + include_directories: [common_includes, xdp_utils_includes], + install: enable_installed_tests, + install_dir: installed_tests_dir, +) +test( + 'test-permission-store', + test_permission_store, + env: env_tests, + is_parallel: false, + protocol: test_protocol, +) + +test_xdp_utils = executable( + 'test-xdp-utils', + 'test-xdp-utils.c', + 'utils.c', + xdp_utils_sources, + sd_escape_sources, + dependencies: [common_deps, libsystemd_dep], + include_directories: [common_includes, xdp_utils_includes], + install: enable_installed_tests, + install_dir: installed_tests_dir, +) +test( + 'test-xdp-utils', + test_xdp_utils, + env: env_tests, + is_parallel: false, + protocol: test_protocol, +) + +pytest = find_program('pytest-3', 'pytest', required: get_option('pytest')) +pymod = import('python') +python = pymod.find_installation( + 'python3', + modules: ['dbus', 'dbusmock', 'gi'], + required: get_option('pytest'), +) + +enable_pytest = pytest.found() and python.found() + +if enable_pytest + subdir('templates') + + pytest_args = ['--verbose', '--log-level=DEBUG'] + + # pytest xdist is nice because it significantly speeds up our + # test process, but it's not required + if pymod.find_installation('python3', modules: ['xdist'], required: false).found() + pytest_args += ['-n', 'auto'] + endif + + pytest_files = [ + 'conftest.py', + '__init__.py', + 'test_clipboard.py', + 'test_email.py', + 'test_globalshortcuts.py', + 'test_inputcapture.py', + 'test_remotedesktop.py', + 'test_trash.py', + ] + foreach pytest_file : pytest_files + configure_file( + input: pytest_file, + output: pytest_file, + copy: true, + install: false + ) + + if pytest_file.startswith('test_') + testname = pytest_file.replace('.py', '') + test( + 'pytest @0@'.format(testname), + pytest, + args: pytest_args + ['-k', testname], + suite: ['pytest'], + timeout: 120, + ) + endif + endforeach +endif + +if enable_installed_tests + # autotools used to symlink to the host files, here we just install our version + install_data( + doc_portal_service_file, + permission_portal_service_file, + install_dir: installed_tests_dir / 'services', + ) + + testfiles = [ + 'testdb', + 'test-doc-portal', + 'test-document-fuse.sh', + 'test-permission-store', + 'test-xdp-utils', + ] + foreach tf : testfiles + data = configuration_data() + data.set('installed_testdir', installed_tests_dir) + data.set('exec', tf) + configure_file( + input: 'template.test.in', + output: '@0@.test'.format(tf), + configuration: data, + install: true, + install_dir: installed_tests_data_dir, + ) + endforeach + + foreach p : portal_tests + data = configuration_data() + data.set('installed_testdir', installed_tests_dir) + data.set('exec', 'test-portals -p /portal/@0@'.format(p)) + configure_file( + input: 'template.test.in', + output: 'test-portals-@0@.test'.format(p), + configuration: data, + install: true, + install_dir: installed_tests_data_dir, + ) + endforeach + + foreach p : portal_limited + data = configuration_data() + data.set('installed_testdir', installed_tests_dir) + data.set('exec', 'test-portals -p /limited/@0@'.format(p)) + configure_file( + input: 'template.test.in', + output: 'test-limited-@0@.test'.format(p), + configuration: data, + install: true, + install_dir: installed_tests_data_dir, + ) + endforeach +endif diff --git a/tests/notification.c b/tests/notification.c new file mode 100644 index 0000000..d47c953 --- /dev/null +++ b/tests/notification.c @@ -0,0 +1,256 @@ + +#include + +#include "account.h" + +#include +#include "xdp-utils.h" + +extern char outdir[]; + +static int got_info; + +static void +notification_action_invoked (XdpPortal *portal, + const char *id, + const char *action, + GVariant *parameter, + gpointer data) +{ + GKeyFile *keyfile = data; + g_autofree char *exp_id = NULL; + g_autofree char *exp_action = NULL; + + exp_id = g_key_file_get_string (keyfile, "notification", "id", NULL); + exp_action = g_key_file_get_string (keyfile, "notification", "action", NULL); + + g_assert_cmpstr (exp_id, ==, id); + g_assert_cmpstr (exp_action, ==, action); + + got_info++; + + g_main_context_wakeup (NULL); +} + +void +test_notification_basic (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + g_autoptr(GVariant) notification = NULL; + const char *notification_s; + gulong id; + + notification_s = "{ 'title': <'title'>, " + " 'body': <'test notification body'>, " + " 'priority': <'normal'>, " + " 'default-action': <'test-action'> }"; + + notification = g_variant_parse (G_VARIANT_TYPE_VARDICT, notification_s, NULL, NULL, NULL); + + keyfile = g_key_file_new (); + + g_key_file_set_string (keyfile, "notification", "data", notification_s); + g_key_file_set_string (keyfile, "notification", "id", "test"); + g_key_file_set_string (keyfile, "notification", "action", "test-action"); + g_key_file_set_integer (keyfile, "backend", "delay", 200); + + path = g_build_filename (outdir, "notification", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + id = g_signal_connect (portal, "notification-action-invoked", G_CALLBACK (notification_action_invoked), keyfile); + + got_info = 0; + xdp_portal_add_notification (portal, "test", notification, 0, NULL, NULL, NULL); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); + + g_signal_handler_disconnect (portal, id); + + xdp_portal_remove_notification (portal, "test"); +} + +void +test_notification_buttons (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + g_autoptr(GVariant) notification = NULL; + const char *notification_s; + gulong id; + + notification_s = "{ 'title': <'test notification 2'>, " + " 'body': <'test notification body 2'>, " + " 'priority': <'low'>, " + " 'default-action': <'test-action'>, " + " 'buttons': <[{'label': <'button1'>, 'action': <'action1'>}, " + " {'label': <'button2'>, 'action': <'action2'>}]> " + "}"; + + notification = g_variant_parse (G_VARIANT_TYPE_VARDICT, notification_s, NULL, NULL, &error); + g_assert_no_error (error); + + keyfile = g_key_file_new (); + + g_key_file_set_string (keyfile, "notification", "data", notification_s); + g_key_file_set_string (keyfile, "notification", "id", "test2"); + g_key_file_set_string (keyfile, "notification", "action", "action1"); + g_key_file_set_integer (keyfile, "backend", "delay", 200); + + path = g_build_filename (outdir, "notification", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + id = g_signal_connect (portal, "notification-action-invoked", G_CALLBACK (notification_action_invoked), keyfile); + + got_info = 0; + xdp_portal_add_notification (portal, "test2", notification, 0, NULL, NULL, NULL); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); + + g_signal_handler_disconnect (portal, id); + + xdp_portal_remove_notification (portal, "test2"); +} + +static void +notification_fail (GObject *source, + GAsyncResult *result, + gpointer data) +{ + XdpPortal *portal = XDP_PORTAL (source); + g_autoptr(GError) error = NULL; + gboolean res; + + res = xdp_portal_add_notification_finish (portal, result, &error); + g_assert_false (res); + g_assert_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT); + + got_info++; + g_main_context_wakeup (NULL); +} + +void +test_notification_bad_arg (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + g_autoptr(GVariant) notification = NULL; + const char *notification_s; + + notification_s = "{ 'title': <'test notification 3'>, " + " 'bodx': <'test notification body 3'> " + "}"; + + notification = g_variant_parse (G_VARIANT_TYPE_VARDICT, notification_s, NULL, NULL, &error); + g_assert_no_error (error); + + keyfile = g_key_file_new (); + + g_key_file_set_string (keyfile, "notification", "data", notification_s); + g_key_file_set_string (keyfile, "notification", "id", "test2"); + g_key_file_set_string (keyfile, "notification", "action", "action1"); + g_key_file_set_boolean (keyfile, "backend", "expect-no-call", TRUE); + + path = g_build_filename (outdir, "notification", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_add_notification (portal, "test3", notification, 0, NULL, notification_fail, NULL); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_notification_bad_priority (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + g_autoptr(GVariant) notification = NULL; + const char *notification_s; + + notification_s = "{ 'title': <'test notification 2'>, " + " 'body': <'test notification body 2'>, " + " 'priority': <'invalid'> " + "}"; + + notification = g_variant_parse (G_VARIANT_TYPE_VARDICT, notification_s, NULL, NULL, &error); + g_assert_no_error (error); + + keyfile = g_key_file_new (); + + g_key_file_set_string (keyfile, "notification", "data", notification_s); + g_key_file_set_string (keyfile, "notification", "id", "test2"); + g_key_file_set_string (keyfile, "notification", "action", "action1"); + g_key_file_set_boolean (keyfile, "backend", "expect-no-call", TRUE); + + path = g_build_filename (outdir, "notification", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_add_notification (portal, "test4", notification, 0, NULL, notification_fail, NULL); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_notification_bad_button (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + g_autoptr(GVariant) notification = NULL; + const char *notification_s; + + notification_s = "{ 'title': <'test notification 5'>, " + " 'body': <'test notification body 5'>, " + " 'buttons': <[{'labex': <'button1'>, 'action': <'action1'>}, " + " {'label': <'button2'>, 'action': <'action2'>}]> " + "}"; + + notification = g_variant_parse (G_VARIANT_TYPE_VARDICT, notification_s, NULL, NULL, &error); + g_assert_no_error (error); + + keyfile = g_key_file_new (); + + g_key_file_set_string (keyfile, "notification", "data", notification_s); + g_key_file_set_string (keyfile, "notification", "id", "test2"); + g_key_file_set_string (keyfile, "notification", "action", "action1"); + g_key_file_set_boolean (keyfile, "backend", "expect-no-call", TRUE); + + path = g_build_filename (outdir, "notification", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_add_notification (portal, "test5", notification, 0, NULL, notification_fail, NULL); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} diff --git a/tests/notification.h b/tests/notification.h new file mode 100644 index 0000000..b22fbbf --- /dev/null +++ b/tests/notification.h @@ -0,0 +1,8 @@ + +#pragma once + +void test_notification_basic (void); +void test_notification_buttons (void); +void test_notification_bad_arg (void); +void test_notification_bad_priority (void); +void test_notification_bad_button (void); diff --git a/tests/openuri.c b/tests/openuri.c new file mode 100644 index 0000000..b78b37e --- /dev/null +++ b/tests/openuri.c @@ -0,0 +1,480 @@ +#include + +#include "openuri.h" + +#include +#include "xdp-utils.h" +#include "xdp-impl-dbus.h" + +#include "utils.h" + +extern XdpDbusImplLockdown *lockdown; +extern XdpDbusImplPermissionStore *permission_store; + +extern char outdir[]; + +static int got_info = 0; + +static void +set_openuri_permissions (const char *type, + const char *handler, + guint count, + guint threshold) +{ + g_autoptr(GError) error = NULL; + g_autofree char *count_s = g_strdup_printf ("%u", count); + g_autofree char *threshold_s = g_strdup_printf ("%u", threshold); + const char *permissions[4]; + + permissions[0] = handler; + permissions[1] = count_s; + permissions[2] = threshold_s; + permissions[3] = NULL; + + xdp_dbus_impl_permission_store_call_delete_sync (permission_store, + "desktop-used-apps", + type, + NULL, + NULL); + + xdp_dbus_impl_permission_store_call_set_permission_sync (permission_store, + "desktop-used-apps", + TRUE, + type, + "", + permissions, + NULL, + &error); + g_assert_no_error (error); +} + +static void +unset_openuri_permissions (const char *type) +{ + xdp_dbus_impl_permission_store_call_delete_sync (permission_store, + "desktop-used-apps", + type, + NULL, + NULL); + /* Ignore the error here, since this fails if the table doesn't exist */ +} + +static void +enable_paranoid_mode (const char *type) +{ + GVariantBuilder data_builder; + + /* turn on paranoid mode to ensure we get a backend call */ + g_variant_builder_init (&data_builder, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add (&data_builder, "{sv}", "always-ask", g_variant_new_boolean (TRUE)); + xdp_dbus_impl_permission_store_call_set_value_sync (permission_store, + "desktop-used-apps", + TRUE, + type, + g_variant_new_variant (g_variant_builder_end (&data_builder)), + NULL, + NULL); +} + +static void +open_uri_cb (GObject *obj, + GAsyncResult *result, + gpointer data) +{ + XdpPortal *portal = XDP_PORTAL (obj); + g_autoptr(GError) error = NULL; + GKeyFile *keyfile = data; + gboolean ret; + int response; + int domain; + int code; + + response = g_key_file_get_integer (keyfile, "result", "response", NULL); + domain = g_key_file_get_integer (keyfile, "result", "error_domain", NULL); + code = g_key_file_get_integer (keyfile, "result", "error_code", NULL); + + ret = xdp_portal_open_uri_finish (portal, result, &error); + if (response == 0) + { + g_assert_no_error (error); + g_assert_true (ret); + } + else if (response == 1) + { + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED); + g_assert_false (ret); + } + else if (response == 2) + { + g_assert_error (error, domain, code); + g_assert_false (ret); + } + else + g_assert_not_reached (); + + got_info++; + + g_main_context_wakeup (NULL); +} + +void +test_open_uri_http (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + unset_openuri_permissions ("x-scheme-handler/http"); + enable_paranoid_mode ("x-scheme-handler/http"); + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "appchooser", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_open_uri (portal, NULL, "http://www.flatpak.org", 0, NULL, open_uri_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_open_uri_http2 (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + g_autoptr(GAppInfo) app = NULL; + g_autofree char *app_id = NULL; + + /* get furrfix.desktop as an app */ + app = g_app_info_get_default_for_type ("x-scheme-handler/xdg-desktop-portal-test", FALSE); + g_assert_nonnull (app); + + app_id = g_strndup (g_app_info_get_id (app), strlen (g_app_info_get_id (app)) - strlen (".desktop")); + + unset_openuri_permissions ("text/plain"); + set_openuri_permissions ("x-scheme-handler/http", app_id, 3, 3); + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_boolean (keyfile, "backend", "expect-no-call", 1); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "appchooser", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_open_uri (portal, NULL, "http://www.flatpak.org", 0, NULL, open_uri_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_open_uri_file (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + g_autofree char *uri = NULL; + + unset_openuri_permissions ("text/plain"); + enable_paranoid_mode ("text/plain"); + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "appchooser", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + g_free (path); + path = g_build_filename (outdir, "test.txt", NULL); + g_file_set_contents (path, "text", -1, &error); + g_assert_no_error (error); + + uri = g_strconcat ("file://", path, NULL); + + got_info = 0; + xdp_portal_open_uri (portal, NULL, uri, 0, NULL, open_uri_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_open_uri_delay (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + g_autofree char *uri = NULL; + + unset_openuri_permissions ("text/plain"); + enable_paranoid_mode ("text/plain"); + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "appchooser", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + g_free (path); + path = g_build_filename (outdir, "test.txt", NULL); + g_file_set_contents (path, "text", -1, &error); + g_assert_no_error (error); + + uri = g_strconcat ("file://", path, NULL); + + got_info = 0; + xdp_portal_open_uri (portal, NULL, uri, 0, NULL, open_uri_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_open_uri_cancel (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + g_autofree char *uri = NULL; + + unset_openuri_permissions ("text/plain"); + enable_paranoid_mode ("text/plain"); + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 1); + g_key_file_set_integer (keyfile, "result", "response", 1); + + path = g_build_filename (outdir, "appchooser", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + g_free (path); + path = g_build_filename (outdir, "test.txt", NULL); + g_file_set_contents (path, "text", -1, &error); + g_assert_no_error (error); + + uri = g_strconcat ("file://", path, NULL); + + got_info = 0; + xdp_portal_open_uri (portal, NULL, uri, 0, NULL, open_uri_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +static gboolean +cancel_call (gpointer data) +{ + GCancellable *cancellable = data; + + g_debug ("cancel call"); + g_cancellable_cancel (cancellable); + + return G_SOURCE_REMOVE; +} + +void +test_open_uri_close (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + g_autofree char *uri = NULL; + GCancellable *cancellable; + + unset_openuri_permissions ("text/plain"); + enable_paranoid_mode ("text/plain"); + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_boolean (keyfile, "backend", "expect-close", 1); + g_key_file_set_integer (keyfile, "backend", "response", 1); + g_key_file_set_integer (keyfile, "result", "response", 1); + + path = g_build_filename (outdir, "appchooser", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + cancellable = g_cancellable_new (); + + g_free (path); + path = g_build_filename (outdir, "test.txt", NULL); + g_file_set_contents (path, "text", -1, &error); + g_assert_no_error (error); + + uri = g_strconcat ("file://", path, NULL); + + got_info = 0; + xdp_portal_open_uri (portal, NULL, uri, 0, cancellable, open_uri_cb, keyfile); + + g_timeout_add (100, cancel_call, cancellable); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_open_uri_lockdown (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + tests_set_property_sync (G_DBUS_PROXY (lockdown), + "org.freedesktop.impl.portal.Lockdown", + "disable-application-handlers", + g_variant_new_boolean (TRUE), + &error); + g_assert_no_error (error); + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 2); + g_key_file_set_integer (keyfile, "result", "error_domain", XDG_DESKTOP_PORTAL_ERROR); + g_key_file_set_integer (keyfile, "result", "error_code", XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED); + + path = g_build_filename (outdir, "appchooser", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_open_uri (portal, NULL, "http://www.flatpak.org", 0, NULL, open_uri_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); + + tests_set_property_sync (G_DBUS_PROXY (lockdown), + "org.freedesktop.impl.portal.Lockdown", + "disable-application-handlers", + g_variant_new_boolean (FALSE), + &error); + g_assert_no_error (error); +} + +static void +open_dir_cb (GObject *obj, + GAsyncResult *result, + gpointer data) +{ + XdpPortal *portal = XDP_PORTAL (obj); + g_autoptr(GError) error = NULL; + GKeyFile *keyfile = data; + gboolean ret; + int response; + + response = g_key_file_get_integer (keyfile, "result", "response", NULL); + + ret = xdp_portal_open_directory_finish (portal, result, &error); + if (response == 0) + { + g_assert_no_error (error); + g_assert_true (ret); + } + else if (response == 1) + { + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED); + g_assert_false (ret); + } + else if (response == 2) + { + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_FAILED); + g_assert_false (ret); + } + else + g_assert_not_reached (); + + got_info++; + + g_main_context_wakeup (NULL); +} + +void +test_open_directory (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + g_autofree char *uri = NULL; + g_autoptr(GAppInfo) app = NULL; + + keyfile = g_key_file_new (); + + app = g_app_info_get_default_for_type ("inode/directory", FALSE); + + if (app == NULL) + { + g_test_skip ("No default handler for inode/directory set"); + return; + } + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", app != NULL ? 0 : 2); + + path = g_build_filename (outdir, "appchooser", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + g_free (path); + path = g_build_filename (outdir, "test.txt", NULL); + g_file_set_contents (path, "text", -1, &error); + g_assert_no_error (error); + + uri = g_strconcat ("file://", path, NULL); + + got_info = 0; + xdp_portal_open_directory (portal, NULL, uri, 0, NULL, open_dir_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} diff --git a/tests/openuri.h b/tests/openuri.h new file mode 100644 index 0000000..f5dcf87 --- /dev/null +++ b/tests/openuri.h @@ -0,0 +1,10 @@ +#pragma once + +void test_open_uri_http (void); +void test_open_uri_http2 (void); +void test_open_uri_file (void); +void test_open_uri_delay (void); +void test_open_uri_close (void); +void test_open_uri_cancel (void); +void test_open_uri_lockdown (void); +void test_open_directory (void); diff --git a/tests/portals/limited/limited-portals.conf b/tests/portals/limited/limited-portals.conf new file mode 100644 index 0000000..57b1642 --- /dev/null +++ b/tests/portals/limited/limited-portals.conf @@ -0,0 +1,6 @@ +[preferred] +default=none +org.freedesktop.impl.portal.Account=limited +org.freedesktop.impl.portal.FileChooser=limited +org.freedesktop.impl.portal.Lockdown=limited +org.freedesktop.impl.portal.Settings=limited diff --git a/tests/portals/limited/limited.portal.in b/tests/portals/limited/limited.portal.in new file mode 100644 index 0000000..9e652aa --- /dev/null +++ b/tests/portals/limited/limited.portal.in @@ -0,0 +1,3 @@ +[portal] +DBusName=org.freedesktop.impl.portal.Limited +Interfaces=@PORTALS@ diff --git a/tests/portals/limited/meson.build b/tests/portals/limited/meson.build new file mode 100644 index 0000000..bc4897d --- /dev/null +++ b/tests/portals/limited/meson.build @@ -0,0 +1,13 @@ +limited_portal = configure_file(input: 'limited.portal.in', + output: 'limited.portal', + configuration: test_portal_conf, + install: enable_installed_tests, + install_dir: installed_tests_dir / 'portals/limited', +) + +configure_file(input: 'limited-portals.conf', + output: '@PLAINNAME@', + copy: true, + install: enable_installed_tests, + install_dir: installed_tests_dir / 'portals/limited', +) diff --git a/tests/portals/meson.build b/tests/portals/meson.build new file mode 100644 index 0000000..96b9326 --- /dev/null +++ b/tests/portals/meson.build @@ -0,0 +1,25 @@ +test_portals = [ + 'org.freedesktop.impl.portal.Access', + 'org.freedesktop.impl.portal.Account', + 'org.freedesktop.impl.portal.AppChooser', + 'org.freedesktop.impl.portal.Background', + 'org.freedesktop.impl.portal.Clipboard', + 'org.freedesktop.impl.portal.Email', + 'org.freedesktop.impl.portal.FileChooser', + 'org.freedesktop.impl.portal.GlobalShortcuts', + 'org.freedesktop.impl.portal.Inhibit', + 'org.freedesktop.impl.portal.InputCapture', + 'org.freedesktop.impl.portal.Lockdown', + 'org.freedesktop.impl.portal.Notification', + 'org.freedesktop.impl.portal.Print', + 'org.freedesktop.impl.portal.RemoteDesktop', + 'org.freedesktop.impl.portal.Screenshot', + 'org.freedesktop.impl.portal.Settings', + 'org.freedesktop.impl.portal.Wallpaper', +] + +test_portal_conf = configuration_data() +test_portal_conf.set('PORTALS', ';'.join(test_portals)) + +subdir('test') +subdir('limited') diff --git a/tests/portals/test/meson.build b/tests/portals/test/meson.build new file mode 100644 index 0000000..5dd04b5 --- /dev/null +++ b/tests/portals/test/meson.build @@ -0,0 +1,13 @@ +test_portal = configure_file(input: 'test.portal.in', + output: 'test.portal', + configuration: test_portal_conf, + install: enable_installed_tests, + install_dir: installed_tests_dir / 'portals/test', +) + +configure_file(input: 'test-portals.conf', + output: '@PLAINNAME@', + copy: true, + install: enable_installed_tests, + install_dir: installed_tests_dir / 'portals/test', +) diff --git a/tests/portals/test/test-portals.conf b/tests/portals/test/test-portals.conf new file mode 100644 index 0000000..d2b8e4d --- /dev/null +++ b/tests/portals/test/test-portals.conf @@ -0,0 +1,2 @@ +[preferred] +default=test; diff --git a/tests/portals/test/test.portal.in b/tests/portals/test/test.portal.in new file mode 100644 index 0000000..ba3c8c4 --- /dev/null +++ b/tests/portals/test/test.portal.in @@ -0,0 +1,3 @@ +[portal] +DBusName=org.freedesktop.impl.portal.Test +Interfaces=@PORTALS@ diff --git a/tests/print.c b/tests/print.c new file mode 100644 index 0000000..2a840c1 --- /dev/null +++ b/tests/print.c @@ -0,0 +1,503 @@ +#include + +#include "print.h" + +#include +#include "xdp-utils.h" +#include "xdp-impl-dbus.h" + +#include "utils.h" + +extern XdpDbusImplLockdown *lockdown; + +extern char outdir[]; + +static int got_info; + +static void +prepare_cb (GObject *obj, + GAsyncResult *result, + gpointer data) +{ + XdpPortal *portal = XDP_PORTAL (obj); + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) ret = NULL; + GKeyFile *keyfile = data; + int response; + int domain; + int code; + + response = g_key_file_get_integer (keyfile, "result", "response", NULL); + domain = g_key_file_get_integer (keyfile, "result", "error_domain", NULL); + code = g_key_file_get_integer (keyfile, "result", "error_code", NULL); + + ret = xdp_portal_prepare_print_finish (portal, result, &error); + if (response == 0) + { + int expected, token; + + g_assert_no_error (error); + + expected = g_key_file_get_integer (keyfile, "result", "token", NULL); + g_variant_lookup (ret, "token", "u", &token); + + g_assert_cmpint (expected, ==, token); + } + else if (response == 1) + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED); + else if (response == 2) + g_assert_error (error, domain, code); + else + g_assert_not_reached (); + + got_info++; + + g_main_context_wakeup (NULL); +} + +void +test_prepare_print_basic (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "print", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_prepare_print (portal, NULL, "test", NULL, NULL, 0, NULL, prepare_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_prepare_print_delay (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "print", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_prepare_print (portal, NULL, "test", NULL, NULL, 0, NULL, prepare_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_prepare_print_cancel (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 1); + g_key_file_set_integer (keyfile, "result", "response", 1); + + path = g_build_filename (outdir, "print", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_prepare_print (portal, NULL, "test", NULL, NULL, 0, NULL, prepare_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +static gboolean +cancel_call (gpointer data) +{ + GCancellable *cancellable = data; + + g_debug ("cancel call"); + g_cancellable_cancel (cancellable); + + return G_SOURCE_REMOVE; +} + +void +test_prepare_print_close (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + g_autoptr(GCancellable) cancellable = NULL; + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_boolean (keyfile, "backend", "expect-close", 1); + g_key_file_set_integer (keyfile, "result", "response", 1); + + path = g_build_filename (outdir, "print", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + cancellable = g_cancellable_new (); + + got_info = 0; + xdp_portal_prepare_print (portal, NULL, "test", NULL, NULL, 0, cancellable, prepare_cb, keyfile); + + g_timeout_add (100, cancel_call, cancellable); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_prepare_print_lockdown (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + tests_set_property_sync (G_DBUS_PROXY (lockdown), + "org.freedesktop.impl.portal.Lockdown", + "disable-printing", + g_variant_new_boolean (TRUE), + &error); + g_assert_no_error (error); + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 2); + g_key_file_set_integer (keyfile, "result", "error_domain", XDG_DESKTOP_PORTAL_ERROR); + g_key_file_set_integer (keyfile, "result", "error_code", XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED); + + path = g_build_filename (outdir, "print", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_prepare_print (portal, NULL, "test", NULL, NULL, 0, NULL, prepare_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); + + tests_set_property_sync (G_DBUS_PROXY (lockdown), + "org.freedesktop.impl.portal.Lockdown", + "disable-printing", + g_variant_new_boolean (FALSE), + &error); + g_assert_no_error (error); +} + +void +test_prepare_print_results (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + g_key_file_set_integer (keyfile, "result", "token", 123); + + path = g_build_filename (outdir, "print", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_prepare_print (portal, NULL, "test", NULL, NULL, 0, NULL, prepare_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_prepare_print_parallel (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "print", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_prepare_print (portal, NULL, "test", NULL, NULL, 0, NULL, prepare_cb, keyfile); + xdp_portal_prepare_print (portal, NULL, "test", NULL, NULL, 0, NULL, prepare_cb, keyfile); + xdp_portal_prepare_print (portal, NULL, "test", NULL, NULL, 0, NULL, prepare_cb, keyfile); + + while (got_info < 3) + g_main_context_iteration (NULL, TRUE); +} + +/* test of Print below */ + +static void +print_cb (GObject *obj, + GAsyncResult *result, + gpointer data) +{ + XdpPortal *portal = XDP_PORTAL (obj); + g_autoptr(GError) error = NULL; + GKeyFile *keyfile = data; + int response; + int domain; + int code; + + response = g_key_file_get_integer (keyfile, "result", "response", NULL); + domain = g_key_file_get_integer (keyfile, "result", "error_domain", NULL); + code = g_key_file_get_integer (keyfile, "result", "error_code", NULL); + + xdp_portal_print_file_finish (portal, result, &error); + if (response == 0) + { + g_assert_no_error (error); + } + else if (response == 1) + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED); + else if (response == 2) + g_assert_error (error, domain, code); + else + g_assert_not_reached (); + + got_info++; + + g_main_context_wakeup (NULL); +} + +void +test_print_basic (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "print", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_print_file (portal, NULL, "test", 0, path, 0, NULL, print_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_print_delay (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "print", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_print_file (portal, NULL, "test", 0, path, 0, NULL, print_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_print_cancel (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 1); + g_key_file_set_integer (keyfile, "result", "response", 1); + + path = g_build_filename (outdir, "print", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_print_file (portal, NULL, "test", 0, path, 0, NULL, print_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_print_close (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + g_autoptr(GCancellable) cancellable = NULL; + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_boolean (keyfile, "backend", "expect-close", 1); + g_key_file_set_integer (keyfile, "result", "response", 1); + + path = g_build_filename (outdir, "print", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + cancellable = g_cancellable_new (); + + got_info = 0; + xdp_portal_print_file (portal, NULL, "test", 0, path, 0, cancellable, print_cb, keyfile); + + g_timeout_add (100, cancel_call, cancellable); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_print_lockdown (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + tests_set_property_sync (G_DBUS_PROXY (lockdown), + "org.freedesktop.impl.portal.Lockdown", + "disable-printing", + g_variant_new_boolean (TRUE), + &error); + g_assert_no_error (error); + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 2); + g_key_file_set_integer (keyfile, "result", "error_domain", XDG_DESKTOP_PORTAL_ERROR); + g_key_file_set_integer (keyfile, "result", "error_code", XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED); + + path = g_build_filename (outdir, "print", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_print_file (portal, NULL, "test", 0, path, 0, NULL, print_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); + + tests_set_property_sync (G_DBUS_PROXY (lockdown), + "org.freedesktop.impl.portal.Lockdown", + "disable-printing", + g_variant_new_boolean (FALSE), + &error); + g_assert_no_error (error); +} + +void +test_print_parallel (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "print", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_print_file (portal, NULL, "test", 0, path, 0, NULL, print_cb, keyfile); + xdp_portal_print_file (portal, NULL, "test", 0, path, 0, NULL, print_cb, keyfile); + xdp_portal_print_file (portal, NULL, "test", 0, path, 0, NULL, print_cb, keyfile); + + while (got_info < 3) + g_main_context_iteration (NULL, TRUE); +} + diff --git a/tests/print.h b/tests/print.h new file mode 100644 index 0000000..07ad21f --- /dev/null +++ b/tests/print.h @@ -0,0 +1,16 @@ +#pragma once + +void test_prepare_print_basic (void); +void test_prepare_print_delay (void); +void test_prepare_print_cancel (void); +void test_prepare_print_close (void); +void test_prepare_print_lockdown (void); +void test_prepare_print_results (void); +void test_prepare_print_parallel (void); + +void test_print_basic (void); +void test_print_delay (void); +void test_print_cancel (void); +void test_print_close (void); +void test_print_lockdown (void); +void test_print_parallel (void); diff --git a/tests/screenshot.c b/tests/screenshot.c new file mode 100644 index 0000000..8c4a933 --- /dev/null +++ b/tests/screenshot.c @@ -0,0 +1,503 @@ +#include + +#include "screenshot.h" + +#include +#include "xdp-impl-dbus.h" + +extern char outdir[]; + +static int got_info; + +extern XdpDbusImplPermissionStore *permission_store; + +static void +set_screenshot_permissions (const char *permission) +{ + const char *permissions[2] = { NULL, NULL }; + g_autoptr(GError) error = NULL; + + permissions[0] = permission; + xdp_dbus_impl_permission_store_call_set_permission_sync (permission_store, + "screenshot", + TRUE, + "screenshot", + "", + permissions, + NULL, + &error); + g_assert_no_error (error); +} + +static void +reset_screenshot_permissions (void) +{ + set_screenshot_permissions (NULL); +} + +static void +screenshot_cb (GObject *obj, + GAsyncResult *result, + gpointer data) +{ + XdpPortal *portal = XDP_PORTAL (obj); + g_autoptr(GError) error = NULL; + GKeyFile *keyfile = data; + int response; + g_autofree char *ret = NULL; + g_autofree char *uri = NULL; + + response = g_key_file_get_integer (keyfile, "result", "response", NULL); + uri = g_key_file_get_string (keyfile, "result", "uri", NULL); + + ret = xdp_portal_take_screenshot_finish (portal, result, &error); + + if (response == 0) + { + g_assert_no_error (error); + g_assert_cmpstr (ret, ==, uri); + } + else if (response == 1) + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED); + else if (response == 2) + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_FAILED); + else + g_assert_not_reached (); + + got_info++; + + g_main_context_wakeup (NULL); +} + +void +test_screenshot_basic (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "access", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + g_free (path); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_string (keyfile, "result", "uri", "file://test/image"); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "screenshot", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_take_screenshot (portal, NULL, 0, NULL, screenshot_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +/* test that everything works as expected when the + * backend takes some time to send its response, as + * is to be expected from a real backend that presents + * dialogs to the user. + */ +void +test_screenshot_delay (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + reset_screenshot_permissions (); + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "access", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + g_free (path); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + g_key_file_set_string (keyfile, "result", "uri", "file://test/image"); + + path = g_build_filename (outdir, "screenshot", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_take_screenshot (portal, NULL, 0, NULL, screenshot_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +/* Test that user cancellation works as expected. + * We simulate that the user cancels a hypothetical dialog, + * by telling the backend to return 1 as response code. + * And we check that we get the expected G_IO_ERROR_CANCELLED. + */ +void +test_screenshot_cancel (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + reset_screenshot_permissions (); + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "access", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + g_free (path); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 1); + g_key_file_set_integer (keyfile, "result", "response", 1); + + path = g_build_filename (outdir, "screenshot", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_take_screenshot (portal, NULL, 0, NULL, screenshot_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +static gboolean +cancel_call (gpointer data) +{ + GCancellable *cancellable = data; + + g_debug ("cancel call"); + g_cancellable_cancel (cancellable); + + return G_SOURCE_REMOVE; +} + +/* Test that app-side cancellation works as expected. + * We cancel the cancellable while while the hypothetical + * dialog is up, and tell the backend that it should + * expect a Close call. We rely on the backend to + * verify that that call actually happened. + */ +void +test_screenshot_close (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + g_autoptr(GCancellable) cancellable = NULL; + + reset_screenshot_permissions (); + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "access", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + g_free (path); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_boolean (keyfile, "backend", "expect-close", 1); + g_key_file_set_integer (keyfile, "result", "response", 1); + + path = g_build_filename (outdir, "screenshot", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + cancellable = g_cancellable_new (); + + got_info = 0; + xdp_portal_take_screenshot (portal, NULL, 0, cancellable, screenshot_cb, keyfile); + + g_timeout_add (100, cancel_call, cancellable); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_screenshot_parallel (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + reset_screenshot_permissions (); + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "access", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + g_free (path); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_string (keyfile, "result", "uri", "file://test/image"); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "screenshot", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_take_screenshot (portal, NULL, 0, NULL, screenshot_cb, keyfile); + xdp_portal_take_screenshot (portal, NULL, 0, NULL, screenshot_cb, keyfile); + xdp_portal_take_screenshot (portal, NULL, 0, NULL, screenshot_cb, keyfile); + + while (got_info < 3) + g_main_context_iteration (NULL, TRUE); +} + +/* Tests for PickColor below */ + +static void +pick_color_cb (GObject *obj, + GAsyncResult *result, + gpointer data) +{ + XdpPortal *portal = XDP_PORTAL (obj); + g_autoptr(GError) error = NULL; + GKeyFile *keyfile = data; + int response; + g_autoptr(GVariant) ret = NULL; + double red, green, blue; + g_autoptr(GVariant) expected = NULL; + + red = g_key_file_get_double (keyfile, "result", "red", NULL); + green = g_key_file_get_double (keyfile, "result", "green", NULL); + blue = g_key_file_get_double (keyfile, "result", "blue", NULL); + expected = g_variant_new ("(ddd)", red, green, blue); + + response = g_key_file_get_integer (keyfile, "result", "response", NULL); + ret = xdp_portal_pick_color_finish (portal, result, &error); + + if (response == 0) + { + g_assert_no_error (error); + g_assert_true (g_variant_equal (ret, expected)); + } + else if (response == 1) + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED); + else if (response == 2) + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_FAILED); + else + g_assert_not_reached (); + + got_info++; + + g_main_context_wakeup (NULL); +} + +void +test_color_basic (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + g_key_file_set_double (keyfile, "result", "red", 0.3); + g_key_file_set_double (keyfile, "result", "green", 0.5); + g_key_file_set_double (keyfile, "result", "blue", 0.7); + + path = g_build_filename (outdir, "screenshot", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_pick_color (portal, NULL, NULL, pick_color_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +/* test that everything works as expected when the + * backend takes some time to send its response, as + * is to be expected from a real backend that presents + * dialogs to the user. + */ +void +test_color_delay (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + keyfile = g_key_file_new (); + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + g_key_file_set_double (keyfile, "result", "red", 0.2); + g_key_file_set_double (keyfile, "result", "green", 0.3); + g_key_file_set_double (keyfile, "result", "blue", 0.4); + + path = g_build_filename (outdir, "screenshot", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_pick_color (portal, NULL, NULL, pick_color_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +/* Test that user cancellation works as expected. + * We simulate that the user cancels a hypothetical dialog, + * by telling the backend to return 1 as response code. + * And we check that we get the expected G_IO_ERROR_CANCELLED. + */ +void +test_color_cancel (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + keyfile = g_key_file_new (); + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 1); + g_key_file_set_integer (keyfile, "result", "response", 1); + + path = g_build_filename (outdir, "screenshot", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_pick_color (portal, NULL, NULL, pick_color_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +/* Test that app-side cancellation works as expected. + * We cancel the cancellable while while the hypothetical + * dialog is up, and tell the backend that it should + * expect a Close call. We rely on the backend to + * verify that that call actually happened. + */ +void +test_color_close (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + g_autoptr(GCancellable) cancellable = NULL; + + keyfile = g_key_file_new (); + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_boolean (keyfile, "backend", "expect-close", 1); + g_key_file_set_integer (keyfile, "result", "response", 1); + + path = g_build_filename (outdir, "screenshot", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + cancellable = g_cancellable_new (); + + got_info = 0; + xdp_portal_pick_color (portal, NULL, cancellable, pick_color_cb, keyfile); + + g_timeout_add (100, cancel_call, cancellable); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_color_parallel (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + + set_screenshot_permissions ("no"); + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + g_key_file_set_double (keyfile, "result", "red", 0.3); + g_key_file_set_double (keyfile, "result", "green", 0.5); + g_key_file_set_double (keyfile, "result", "blue", 0.7); + + path = g_build_filename (outdir, "screenshot", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + got_info = 0; + xdp_portal_pick_color (portal, NULL, NULL, pick_color_cb, keyfile); + xdp_portal_pick_color (portal, NULL, NULL, pick_color_cb, keyfile); + xdp_portal_pick_color (portal, NULL, NULL, pick_color_cb, keyfile); + + while (got_info < 3) + g_main_context_iteration (NULL, TRUE); +} diff --git a/tests/screenshot.h b/tests/screenshot.h new file mode 100644 index 0000000..8d03c1a --- /dev/null +++ b/tests/screenshot.h @@ -0,0 +1,13 @@ +#pragma once + +void test_screenshot_basic (void); +void test_screenshot_delay (void); +void test_screenshot_cancel (void); +void test_screenshot_close (void); +void test_screenshot_parallel (void); + +void test_color_basic (void); +void test_color_delay (void); +void test_color_cancel (void); +void test_color_close (void); +void test_color_parallel (void); diff --git a/tests/services/meson.build b/tests/services/meson.build new file mode 100644 index 0000000..7b0dc8d --- /dev/null +++ b/tests/services/meson.build @@ -0,0 +1,11 @@ +conf_service = configuration_data() +conf_service.set('libexecdir', meson.project_build_root() / 'document-portal') + +configure_file(input: doc_portal_service_file_sources, + output: '@BASENAME@', + configuration: conf_service, + ) +configure_file(input: permission_portal_service_file_sources, + output: '@BASENAME@', + configuration: conf_service, + ) diff --git a/tests/session.conf.in b/tests/session.conf.in new file mode 100644 index 0000000..307f84a --- /dev/null +++ b/tests/session.conf.in @@ -0,0 +1,56 @@ + + + + session + + + + + unix:tmpdir=/tmp + + @testdir@/services + + + + + + + + + + + + + + contexts/dbus_contexts + + + + + 1000000000 + 250000000 + 1000000000 + 250000000 + 1000000000 + 4096 + 120000 + 240000 + 100000 + 10000 + 100000 + 10000 + 50000 + 50000 + 50000 + + diff --git a/tests/share/applications/furrfix.desktop b/tests/share/applications/furrfix.desktop new file mode 100644 index 0000000..f35caf0 --- /dev/null +++ b/tests/share/applications/furrfix.desktop @@ -0,0 +1,13 @@ +[Desktop Entry] +Version=1.0 +Name=Furrfix +GenericName=Not a Web Browser +Comment=Don't Browse the Web +Exec=true %u +Icon=furrfix +Terminal=false +Type=Application +MimeType=text/html;text/xml;application/xhtml+xml;application/vnd.mozilla.xul+xml;text/mml;x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/xdg-desktop-portal-test; +StartupNotify=true +Categories=Network;WebBrowser; +Keywords=web;browser;internet; diff --git a/tests/share/applications/meson.build b/tests/share/applications/meson.build new file mode 100644 index 0000000..d56b633 --- /dev/null +++ b/tests/share/applications/meson.build @@ -0,0 +1,2 @@ +configure_file(input: 'furrfix.desktop', output: '@PLAINNAME@', copy: true) +configure_file(input: 'mimeinfo.cache', output: '@PLAINNAME@', copy: true) diff --git a/tests/share/applications/mimeinfo.cache b/tests/share/applications/mimeinfo.cache new file mode 100644 index 0000000..7881f39 --- /dev/null +++ b/tests/share/applications/mimeinfo.cache @@ -0,0 +1,9 @@ +[MIME Cache] +application/vnd.mozilla.xul+xml=furrfix.desktop; +application/xhtml+xml=furrfix.desktop; +text/html=furrfix.desktop; +text/mml=furrfix.desktop; +text/xml=furrfix.desktop; +x-scheme-handler/http=furrfix.desktop; +x-scheme-handler/https=furrfix.desktop; +x-scheme-handler/xdg-desktop-portal-test=furrfix.desktop; diff --git a/tests/share/meson.build b/tests/share/meson.build new file mode 100644 index 0000000..48c4428 --- /dev/null +++ b/tests/share/meson.build @@ -0,0 +1 @@ +subdir('applications') diff --git a/tests/template.test.in b/tests/template.test.in new file mode 100644 index 0000000..01c1e19 --- /dev/null +++ b/tests/template.test.in @@ -0,0 +1,4 @@ +[Test] +Type=session +Exec=@installed_testdir@/@exec@ --tap +Output=TAP diff --git a/tests/templates/__init__.py b/tests/templates/__init__.py new file mode 100644 index 0000000..0ce724b --- /dev/null +++ b/tests/templates/__init__.py @@ -0,0 +1,227 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is formatted with Python Black + +from typing import Callable, Dict, Optional, NamedTuple +import dbus +import dbusmock +import logging + + +def init_template_logger(name: str): + """ + Common logging setup for the impl.portal templates. Use as: + + >>> from tests.templates import init_template_logger + >>> logger = init_template_logger(__name__) + >>> logger.debug("foo") + + """ + logging.basicConfig( + format="%(levelname).1s|%(name)s: %(message)s", level=logging.DEBUG + ) + logger = logging.getLogger(f"templates.{name}") + logger.setLevel(logging.DEBUG) + return logger + + +logger = init_template_logger("request") + + +class Response(NamedTuple): + response: int + results: Dict + + +class ImplRequest: + """ + Implementation of a org.freedesktop.impl.portal.Request object. Typically + this object needs to be merely exported: + + >>> r = ImplRequest(mock, "org.freedesktop.impl.portal.Test", handle) + >>> r.export() + + Where the test or the backend implementation relies on the Closed() method + of the ImplRequest, provide a callback to be invoked. + + >>> r.export(close_callback=my_callback) + + Note that the latter only works if the test invokes methods + asynchronously. + + .. attribute:: closed + + Set to True if the Close() method on the Request was invoked + + """ + + def __init__(self, mock: "dbusmock.DBusMockObject", busname: str, handle: str): + self.mock = mock + self.handle = handle + self.closed = False + self._close_callback: Optional[Callable] = None + + bus = mock.connection + proxy = bus.get_object(busname, handle) + mock_interface = dbus.Interface(proxy, dbusmock.MOCK_IFACE) + + # Register for the Close() call on the impl.Request. If it gets + # called, use the side-channel RequestClosed signal so we can notify + # the test that the impl.Request was actually closed by the + # xdg-desktop-portal + def cb_methodcall(name, args): + if name == "Close": + self.closed = True + logger.debug(f"Close() on {self}") + if self._close_callback: + self._close_callback() + self.mock.EmitSignal( + "org.freedesktop.impl.portal.Test", + "RequestClosed", + "s", + (self.handle,), + ) + self.mock.RemoveObject(self.handle) + + mock_interface.connect_to_signal("MethodCalled", cb_methodcall) + + def export(self, close_callback: Optional[Callable] = None): + """ + Create the object on the bus. If close_callback is not None, that + callback will be invoked in response to the Close() method called on + this object. + """ + self.mock.AddObject( + path=self.handle, + interface="org.freedesktop.impl.portal.Request", + properties={}, + methods=[ + ( + "Close", + "", + "", + "", + ) + ], + ) + self._close_callback = close_callback + return self + + def __str__(self): + return f"ImplRequest {self.handle}" + + +class ImplSession: + """ + Implementation of a org.freedesktop.impl.portal.Session object. Do not + instantiate this directly, instead use ``ImplSession.export()``. Typically + like this: + + >>> s = ImplSession.export(mock, "org.freedesktop.impl.portal.Test", "/path/foo") + + Where the test or the backend implementation relies on the Closed() method + of the ImplSession, provide a callback to be invoked. + + >>> r.export(close_callback=my_callback) + + Note that the latter only works if the test invokes methods + asynchronously. + + .. attribute:: closed + + Set to True if the Close() method on the Session was invoked + + .. attribute:: handle + + The session's object path + + """ + + def __init__( + self, + mock: dbusmock.DBusMockObject, + busname: str, + handle: str, + ): + self.mock = mock # the main mock object + self.handle = handle + self.closed = False + self._close_callback: Optional[Callable] = None + + self.mock_object: Optional[dbusmock.DBusMockObject] = None + + bus = mock.connection + proxy = bus.get_object(busname, handle) + mock_interface = dbus.Interface(proxy, dbusmock.MOCK_IFACE) + + # Register for the Close() call on the impl.Session. If it gets + # called, use the side-channel SessionClosed signal so we can notify + # the test that the impl.Session was actually closed by the + # xdg-desktop-portal + def cb_methodcall(name, args): + if name == "Close": + self.closed = True + logger.debug(f"Session.Close() on {self.handle}") + if self._close_callback: + self._close_callback() + self.mock.EmitSignal( + "org.freedesktop.impl.portal.Test", + "SessionClosed", + "s", + (self.handle,), + ) + self._unexport() + + mock_interface.connect_to_signal("MethodCalled", cb_methodcall) + + def export( + self, + close_callback: Optional[Callable] = None, + ) -> "ImplSession": + """ + Create the session on the bus. If ``close_callback`` is not None, that + callback will be invoked in response to the Close() method called on + this object. + """ + self.mock.AddObject( + path=self.handle, + interface="org.freedesktop.impl.portal.Session", + properties={}, + methods=[ + ( + "Close", + "", + "", + "", + ) + ], + ) + # This is a bit awkward. We need our session's DBusMockObject for + # EmitSignal of impl.portal.Session.Close. This is available in + # dbusmock.get_object() since our template runs as part of the server. + # + # In theory, EmitSignal should work on self.mock_interface but + # it doesn't and I can't figure out why. + self.mock_object = dbusmock.get_object(self.handle) + return self + + def _unexport(self): + self.mock.RemoveObject(path=self.handle) + + def close(self): + """ + Send out Closed signal and remove this session from the bus. + """ + assert self.mock_object is not None, "Session was never exported" + logger.debug(f"Signal Session.Closed on {self.handle}") + self.mock_object.EmitSignal( + interface="org.freedesktop.impl.portal.Session", + name="Closed", + signature="", + sigargs=(), + ) + self.closed = True + self._unexport() + + def __str__(self): + return f"ImplSession {self.handle}" diff --git a/tests/templates/clipboard.py b/tests/templates/clipboard.py new file mode 100644 index 0000000..8b55fc8 --- /dev/null +++ b/tests/templates/clipboard.py @@ -0,0 +1,149 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is formatted with Python Black + +from tests.templates import init_template_logger +import dbus.service +import dbus +import tempfile + +from gi.repository import GLib + +BUS_NAME = "org.freedesktop.impl.portal.Test" +MAIN_OBJ = "/org/freedesktop/portal/desktop" +SYSTEM_BUS = False +MAIN_IFACE = "org.freedesktop.impl.portal.Clipboard" +VERSION = 1 + +logger = init_template_logger(__name__) + + +def load(mock, parameters=None): + logger.debug(f"Loading parameters: {parameters}") + + mock.delay: int = parameters.get("delay", 200) + mock.response: int = parameters.get("response", 0) + mock.expect_close: bool = parameters.get("expect-close", False) + + mock.AddProperties( + MAIN_IFACE, + dbus.Dictionary( + { + "version": dbus.UInt32(parameters.get("version", VERSION)), + } + ), + ) + + +@dbus.service.method( + MAIN_IFACE, + in_signature="oa{sv}", + out_signature="", + async_callbacks=("cb_success", "cb_error"), +) +def RequestClipboard(self, session_handle, options, cb_success, cb_error): + try: + logger.debug(f"RequestClipboard({session_handle}, {options})") + + if self.expect_close: + cb_success() + else: + logger.debug(f"scheduling delay of {self.delay}") + GLib.timeout_add(self.delay, cb_success) + except Exception as e: + logger.critical(e) + cb_error(e) + + +@dbus.service.method( + MAIN_IFACE, + in_signature="oa{sv}", + out_signature="", + async_callbacks=("cb_success", "cb_error"), +) +def SetSelection(self, session_handle, options, cb_success, cb_error): + try: + logger.debug(f"SetSelection({session_handle}, {options})") + + if self.expect_close: + cb_success() + else: + logger.debug(f"scheduling delay of {self.delay}") + GLib.timeout_add(self.delay, cb_success) + except Exception as e: + logger.critical(e) + cb_error(e) + + +@dbus.service.method( + MAIN_IFACE, + in_signature="ou", + out_signature="h", + async_callbacks=("cb_success", "cb_error"), +) +def SelectionWrite(self, session_handle, serial, cb_success, cb_error): + try: + logger.debug(f"SelectionWrite({session_handle}, {serial})") + + temp_file = tempfile.TemporaryFile() + fd = dbus.types.UnixFd(temp_file.fileno()) + + if self.expect_close: + cb_success(fd) + else: + + def reply(): + cb_success(fd) + + logger.debug(f"scheduling delay of {self.delay}") + GLib.timeout_add(self.delay, reply) + except Exception as e: + logger.critical(e) + cb_error(e) + + +@dbus.service.method( + MAIN_IFACE, + in_signature="oub", + out_signature="", + async_callbacks=("cb_success", "cb_error"), +) +def SelectionWriteDone(self, session_handle, serial, success, cb_success, cb_error): + try: + logger.debug(f"SelectionWriteDone({session_handle}, {serial}, {success})") + + if self.expect_close: + cb_success() + else: + logger.debug(f"scheduling delay of {self.delay}") + GLib.timeout_add(self.delay, cb_success) + except Exception as e: + logger.critical(e) + cb_error(e) + + +@dbus.service.method( + MAIN_IFACE, + in_signature="os", + out_signature="h", + async_callbacks=("cb_success", "cb_error"), +) +def SelectionRead(self, session_handle, mime_type, cb_success, cb_error): + try: + logger.debug(f"SelectionRead({session_handle}, {mime_type})") + + temp_file = tempfile.TemporaryFile() + fd = dbus.types.UnixFd(temp_file.fileno()) + + if self.expect_close: + cb_success(fd) + else: + + def reply(): + cb_success(fd) + + logger.debug(f"scheduling delay of {self.delay}") + GLib.timeout_add(self.delay, reply) + except Exception as e: + logger.critical(e) + cb_error(e) diff --git a/tests/templates/email.py b/tests/templates/email.py new file mode 100644 index 0000000..466f44e --- /dev/null +++ b/tests/templates/email.py @@ -0,0 +1,70 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is formatted with Python Black + +from tests.templates import Response, init_template_logger, ImplRequest +import dbus.service + +from gi.repository import GLib + + +BUS_NAME = "org.freedesktop.impl.portal.Test" +MAIN_OBJ = "/org/freedesktop/portal/desktop" +SYSTEM_BUS = False +MAIN_IFACE = "org.freedesktop.impl.portal.Email" +VERSION = 3 + + +logger = init_template_logger(__name__) + + +def load(mock, parameters=None): + logger.debug(f"Loading parameters: {parameters}") + + mock.delay: int = parameters.get("delay", 200) + mock.response: int = parameters.get("response", 0) + mock.expect_close: bool = parameters.get("expect-close", False) + mock.AddProperties( + MAIN_IFACE, + dbus.Dictionary( + { + "version": dbus.UInt32(parameters.get("version", VERSION)), + } + ), + ) + + +@dbus.service.method( + MAIN_IFACE, + in_signature="ossa{sv}", + out_signature="ua{sv}", + async_callbacks=("cb_success", "cb_error"), +) +def ComposeEmail(self, handle, app_id, parent_window, options, cb_success, cb_error): + try: + logger.debug(f"ComposeEmail({handle}, {app_id}, {parent_window}, {options})") + + response = Response(self.response, {}) + + request = ImplRequest(self, BUS_NAME, handle) + + if self.expect_close: + + def closed_callback(): + response = Response(2, {}) + logger.debug(f"ComposeEmail Close() response {response}") + cb_success(response.response, response.results) + + request.export(closed_callback) + else: + request.export() + + def reply(): + logger.debug(f"ComposeEmail with response {response}") + cb_success(response.response, response.results) + + logger.debug(f"scheduling delay of {self.delay}") + GLib.timeout_add(self.delay, reply) + except Exception as e: + logger.critical(e) + cb_error(e) diff --git a/tests/templates/globalshortcuts.py b/tests/templates/globalshortcuts.py new file mode 100644 index 0000000..876632d --- /dev/null +++ b/tests/templates/globalshortcuts.py @@ -0,0 +1,162 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is formatted with Python Black + +from tests.templates import Response, init_template_logger, ImplRequest, ImplSession +import dbus +import dbus.service +import time +from dbusmock import MOCK_IFACE + +from gi.repository import GLib + + +BUS_NAME = "org.freedesktop.impl.portal.Test" +MAIN_OBJ = "/org/freedesktop/portal/desktop" +SYSTEM_BUS = False +MAIN_IFACE = "org.freedesktop.impl.portal.GlobalShortcuts" +VERSION = 1 + + +logger = init_template_logger(__name__) + + +def load(mock, parameters): + logger.debug(f"Loading parameters: {parameters}") + + mock.delay: int = parameters.get("delay", 200) + mock.response: int = parameters.get("response", 0) + mock.expect_close: bool = parameters.get("expect-close", False) + mock.force_close: int = parameters.get("force-close", 0) + mock.AddProperties( + MAIN_IFACE, + dbus.Dictionary( + { + "version": dbus.UInt32(parameters.get("version", VERSION)), + } + ), + ) + mock.sessions: dict[str, ImplSession] = {} + + +@dbus.service.method( + MAIN_IFACE, + in_signature="oosa{sv}", + out_signature="ua{sv}", + async_callbacks=("cb_success", "cb_error"), +) +def CreateSession(self, handle, session_handle, app_id, options, cb_success, cb_error): + try: + logger.debug(f"CreateSession({handle}, {session_handle}, {app_id}, {options})") + + session = ImplSession(self, BUS_NAME, session_handle).export() + self.sessions[session_handle] = session + + response = Response(self.response, {"session_handle": session.handle}) + + request = ImplRequest(self, BUS_NAME, handle) + + if self.expect_close: + + def closed_callback(): + response = Response(2, {}) + logger.debug(f"CreateSession Close() response {response}") + cb_success(response.response, response.results) + + request.export(closed_callback) + else: + request.export() + + def reply(): + logger.debug(f"CreateSession with response {response}") + cb_success(response.response, response.results) + + logger.debug(f"scheduling delay of {self.delay}") + GLib.timeout_add(self.delay, reply) + + if self.force_close > 0: + + def force_close(): + session.close() + + GLib.timeout_add(self.force_close, force_close) + + except Exception as e: + logger.critical(e) + cb_error(e) + + +@dbus.service.method( + MAIN_IFACE, + in_signature="ooa(sa{sv})sa{sv}", + out_signature="ua{sv}", + async_callbacks=("cb_success", "cb_error"), +) +def BindShortcuts( + self, + handle, + session_handle, + shortcuts, + parent_window, + options, + cb_success, + cb_error, +): + try: + logger.debug( + f"BindShortcuts({handle}, {session_handle}, {shortcuts}, {options})" + ) + + assert session_handle in self.sessions + response = Response(self.response, {}) + request = ImplRequest(self, BUS_NAME, handle) + request.export() + + def reply(): + logger.debug(f"BindShortcuts with response {response}") + self.sessions[session_handle].shortcuts = shortcuts + cb_success(response.response, response.results) + + logger.debug(f"scheduling delay of {self.delay}") + GLib.timeout_add(self.delay, reply) + + except Exception as e: + logger.critical(e) + cb_error(e) + + +@dbus.service.method( + MAIN_IFACE, + in_signature="oo", + out_signature="ua{sv}", +) +def ListShortcuts( + self, + handle, + session_handle, +): + shortcuts = self.sessions[session_handle].shortcuts + return (0, {"shortcuts": shortcuts}) + + +@dbus.service.method( + MOCK_IFACE, + in_signature="os", + out_signature="", +) +def Trigger(self, session_handle, shortcut_id): + now_since_epoch = int(time.time() * 1000000) + self.EmitSignal( + MAIN_IFACE, + "Activated", + "osta{sv}", + [session_handle, shortcut_id, now_since_epoch, {}], + ) + time.sleep(0.2) + now_since_epoch = int(time.time() * 1000000) + self.EmitSignal( + MAIN_IFACE, + "Deactivated", + "osta{sv}", + [session_handle, shortcut_id, now_since_epoch, {}], + ) diff --git a/tests/templates/inputcapture.py b/tests/templates/inputcapture.py new file mode 100644 index 0000000..e9c2ecc --- /dev/null +++ b/tests/templates/inputcapture.py @@ -0,0 +1,297 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is formatted with Python Black + +from collections import namedtuple +from itertools import count +from gi.repository import GLib + +import dbus +import dbus.service +import logging +import socket + +BUS_NAME = "org.freedesktop.impl.portal.Test" +MAIN_OBJ = "/org/freedesktop/portal/desktop" +SYSTEM_BUS = False +MAIN_IFACE = "org.freedesktop.impl.portal.InputCapture" +VERSION = 1 + +logger = logging.getLogger(f"templates.{__name__}") +logger.setLevel(logging.DEBUG) + +serials = count() + +Response = namedtuple("Response", ["response", "results"]) +Barrier = namedtuple("Barrier", ["id", "position"]) + + +def load(mock, parameters=None): + logger.debug(f"Loading parameters: {parameters}") + # Delay before Request.response + mock.delay: int = parameters.get("delay", 0) + + mock.supported_capabilities = parameters.get("supported_capabilities", 0xF) + # The actual ones we reply with in the CreateSession request + mock.capabilities = parameters.get("capabilities", None) + + mock.default_zone = parameters.get("default-zone", [(1920, 1080, 0, 0)]) + mock.current_zones = mock.default_zone + mock.current_zone_set = next(serials) + + mock.disable_delay = parameters.get("disable-delay", 0) + mock.activated_delay = parameters.get("activated-delay", 0) + mock.deactivated_delay = parameters.get("deactivated-delay", 0) + + mock.AddProperties( + MAIN_IFACE, + dbus.Dictionary( + { + "version": dbus.UInt32(parameters.get("version", VERSION)), + "SupportedCapabilities": dbus.UInt32(mock.supported_capabilities), + } + ), + ) + + mock.active_session_handles = [] + + +@dbus.service.method( + MAIN_IFACE, + in_signature="oossa{sv}", + out_signature="ua{sv}", +) +def CreateSession(self, handle, session_handle, app_id, parent_window, options): + try: + logger.debug(f"CreateSession({parent_window}, {options})") + + assert "capabilities" in options + + # Filter to the subset of supported capabilities + if self.capabilities is None: + capabilities = options["capabilities"] + else: + capabilities = self.capabilities + + capabilities &= self.supported_capabilities + response = Response(0, {}) + + response.results["capabilities"] = dbus.UInt32(capabilities) + self.active_session_handles.append(session_handle) + + logger.debug(f"CreateSession with response {response}") + + return response.response, response.results + except Exception as e: + logger.critical(e) + return (2, {}) + + +@dbus.service.method( + MAIN_IFACE, + in_signature="oosa{sv}", + out_signature="ua{sv}", +) +def GetZones(self, handle, session_handle, app_id, options): + try: + logger.debug(f"GetZones({session_handle}, {options})") + + assert session_handle in self.active_session_handles + + response = Response(0, {}) + response.results["zones"] = self.default_zone + response.results["zone_set"] = dbus.UInt32( + self.current_zone_set, variant_level=1 + ) + logger.debug(f"GetZones with response {response}") + + if response.response == 0: + self.current_zones = response.results["zones"] + + return response.response, response.results + except Exception as e: + logger.critical(e) + return (2, {}) + + +@dbus.service.method( + MAIN_IFACE, + in_signature="oosa{sv}aa{sv}u", + out_signature="ua{sv}", +) +def SetPointerBarriers( + self, handle, session_handle, app_id, options, barriers, zone_set +): + try: + logger.debug( + f"SetPointerBarriers({session_handle}, {options}, {barriers}, {zone_set})" + ) + + assert session_handle in self.active_session_handles + assert zone_set == self.current_zone_set + + self.current_barriers = [] + + failed_barriers = [] + + # Barrier sanity checks: + for b in barriers: + id = b["barrier_id"] + x1, y1, x2, y2 = b["position"] + if (x1 != x2 and y1 != y2) or (x1 == x2 and y1 == y2): + logger.debug(f"Barrier {id} is not horizontal or vertical") + failed_barriers.append(id) + continue + + for z in self.current_zones: + w, h, x, y = z + if x1 < x or x1 > x + w: + continue + if y1 < y or y1 > y + h: + continue + + # x1/y1 fit into our current zone + if x2 < x or x2 > x + w or y2 < y or y2 > y + h: + logger.debug(f"Barrier {id} spans multiple zones") + elif x1 == x2 and (x1 != x and x1 != x + w): + logger.debug(f"Barrier {id} is not on vertical edge") + elif y1 == y2 and (y1 != y and y1 != y + h): + logger.debug(f"Barrier {id} is not on horizontal edge") + else: + self.current_barriers.append(Barrier(id=id, position=b["position"])) + break + + failed_barriers.append(id) + break + else: + logger.debug(f"Barrier {id} does not fit into any zone") + failed_barriers.append(id) + continue + + response = Response(0, {}) + response.results["failed_barriers"] = dbus.Array( + [dbus.UInt32(f) for f in failed_barriers], + signature="u", + variant_level=1, + ) + + logger.debug(f"SetPointerBarriers with response {response}") + + return response.response, response.results + except Exception as e: + logger.critical(e) + return (2, {}) + + +@dbus.service.method( + MAIN_IFACE, + in_signature="osa{sv}", + out_signature="ua{sv}", +) +def Enable(self, session_handle, app_id, options): + try: + logger.debug(f"Enable({session_handle}, {options})") + + assert session_handle in self.active_session_handles + + # for use in the signals + activation_id = next(serials) + barrier = self.current_barriers[0] + pos = (barrier.position[0] + 10, barrier.position[1] + 20) + + if self.disable_delay > 0: + + def disable(): + logger.debug("emitting Disabled") + self.EmitSignal("", "Disabled", "oa{sv}", [session_handle, {}]) + + GLib.timeout_add(self.disable_delay, disable) + + if self.activated_delay > 0: + + def activated(): + logger.debug("emitting Activated") + options = { + "activation_id": dbus.UInt32(activation_id, variant_level=1), + "barrier_id": dbus.UInt32(barrier.id, variant_level=1), + "cursor_position": dbus.Struct( + pos, signature="dd", variant_level=1 + ), + } + self.EmitSignal("", "Activated", "oa{sv}", [session_handle, options]) + + GLib.timeout_add(self.activated_delay, activated) + + if self.deactivated_delay > 0: + + def deactivated(): + logger.debug("emitting Deactivated") + options = { + "activation_id": dbus.UInt32(activation_id, variant_level=1), + "cursor_position": dbus.Struct( + pos, signature="dd", variant_level=1 + ), + } + self.EmitSignal("", "Deactivated", "oa{sv}", [session_handle, options]) + + GLib.timeout_add(self.deactivated_delay, deactivated) + + except Exception as e: + logger.critical(e) + return (2, {}) + + +@dbus.service.method( + MAIN_IFACE, + in_signature="osa{sv}", + out_signature="ua{sv}", +) +def Disable(self, session_handle, app_id, options): + try: + logger.debug(f"Disable({session_handle}, {options})") + + assert session_handle in self.active_session_handles + except Exception as e: + logger.critical(e) + return (2, {}) + + +@dbus.service.method( + MAIN_IFACE, + in_signature="osa{sv}", + out_signature="ua{sv}", +) +def Release(self, session_handle, app_id, options): + try: + logger.debug(f"Release({session_handle}, {options})") + + assert session_handle in self.active_session_handles + except Exception as e: + logger.critical(e) + return (2, {}) + + +@dbus.service.method( + MAIN_IFACE, + in_signature="osa{sv}", + out_signature="h", +) +def ConnectToEIS(self, session_handle, app_id, options): + try: + logger.debug(f"ConnectToEIS({session_handle}, {options})") + + assert session_handle in self.active_session_handles + + sockets = socket.socketpair() + self.eis_socket = sockets[0] + + assert self.eis_socket.send(b"HELLO") == 5 + + fd = sockets[1] + + logger.debug(f"ConnectToEis with fd {fd.fileno()}") + + return dbus.types.UnixFd(fd) + except Exception as e: + logger.critical(e) + return -1 diff --git a/tests/templates/meson.build b/tests/templates/meson.build new file mode 100644 index 0000000..8a1a6a4 --- /dev/null +++ b/tests/templates/meson.build @@ -0,0 +1,16 @@ +template_files = [ + '__init__.py', + 'clipboard.py', + 'email.py', + 'globalshortcuts.py', + 'inputcapture.py', + 'remotedesktop.py', +] +foreach template_file : template_files + configure_file( + input: template_file, + output: template_file, + copy: true, + install: false + ) +endforeach diff --git a/tests/templates/remotedesktop.py b/tests/templates/remotedesktop.py new file mode 100644 index 0000000..e30e7e4 --- /dev/null +++ b/tests/templates/remotedesktop.py @@ -0,0 +1,185 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is formatted with Python Black + +from tests.templates import Response, init_template_logger, ImplRequest, ImplSession +import dbus +import dbus.service +import socket + +from gi.repository import GLib + + +BUS_NAME = "org.freedesktop.impl.portal.Test" +MAIN_OBJ = "/org/freedesktop/portal/desktop" +SYSTEM_BUS = False +MAIN_IFACE = "org.freedesktop.impl.portal.RemoteDesktop" +VERSION = 2 + + +logger = init_template_logger(__name__) + + +def load(mock, parameters): + logger.debug(f"Loading parameters: {parameters}") + + mock.delay: int = parameters.get("delay", 200) + mock.response: int = parameters.get("response", 0) + mock.expect_close: bool = parameters.get("expect-close", False) + mock.force_close: int = parameters.get("force-close", 0) + mock.force_clipoboard_enabled: bool = parameters.get( + "force-clipboard-enabled", False + ) + mock.fail_connect_to_eis: bool = parameters.get("fail-connect-to-eis", False) + mock.AddProperties( + MAIN_IFACE, + dbus.Dictionary( + { + "version": dbus.UInt32(parameters.get("version", VERSION)), + } + ), + ) + mock.sessions: dict[str, ImplSession] = {} + + +@dbus.service.method( + MAIN_IFACE, + in_signature="oosa{sv}", + out_signature="ua{sv}", + async_callbacks=("cb_success", "cb_error"), +) +def CreateSession(self, handle, session_handle, app_id, options, cb_success, cb_error): + try: + logger.debug(f"CreateSession({handle}, {session_handle}, {app_id}, {options})") + + session = ImplSession(self, BUS_NAME, session_handle).export() + self.sessions[session_handle] = session + + response = Response(self.response, {"session_handle": session.handle}) + + request = ImplRequest(self, BUS_NAME, handle) + + if self.expect_close: + + def closed_callback(): + response = Response(2, {}) + logger.debug(f"CreateSession Close() response {response}") + cb_success(response.response, response.results) + + request.export(closed_callback) + else: + request.export() + + def reply(): + logger.debug(f"CreateSession with response {response}") + cb_success(response.response, response.results) + + logger.debug(f"scheduling delay of {self.delay}") + GLib.timeout_add(self.delay, reply) + + if self.force_close > 0: + + def force_close(): + session.close() + + GLib.timeout_add(self.force_close, force_close) + + except Exception as e: + logger.critical(e) + cb_error(e) + + +@dbus.service.method( + MAIN_IFACE, + in_signature="oosa{sv}", + out_signature="ua{sv}", + async_callbacks=("cb_success", "cb_error"), +) +def SelectDevices(self, handle, session_handle, app_id, options, cb_success, cb_error): + try: + logger.debug(f"SelectDevices({handle}, {session_handle}, {app_id}, {options})") + + assert session_handle in self.sessions + response = Response(self.response, {}) + request = ImplRequest(self, BUS_NAME, handle) + request.export() + + def reply(): + logger.debug(f"SelectDevices with response {response}") + cb_success(response.response, response.results) + + logger.debug(f"scheduling delay of {self.delay}") + GLib.timeout_add(self.delay, reply) + + except Exception as e: + logger.critical(e) + cb_error(e) + + +@dbus.service.method( + MAIN_IFACE, + in_signature="oossa{sv}", + out_signature="ua{sv}", + async_callbacks=("cb_success", "cb_error"), +) +def Start( + self, handle, session_handle, app_id, parent_window, options, cb_success, cb_error +): + try: + logger.debug( + f"Start({handle}, {session_handle}, {parent_window}, {app_id}, {options})" + ) + + assert session_handle in self.sessions + response = Response(self.response, {}) + + if self.force_clipoboard_enabled: + response.results["clipboard_enabled"] = True + + request = ImplRequest(self, BUS_NAME, handle) + + if self.expect_close: + + def closed_callback(): + response = Response(2, {}) + logger.debug(f"Start Close() response {response}") + cb_success(response.response, response.results) + + request.export(closed_callback) + else: + request.export() + + def reply(): + logger.debug(f"Start with response {response}") + cb_success(response.response, response.results) + + logger.debug(f"scheduling delay of {self.delay}") + GLib.timeout_add(self.delay, reply) + + except Exception as e: + logger.critical(e) + cb_error(e) + + +@dbus.service.method( + MAIN_IFACE, + in_signature="osa{sv}", + out_signature="h", +) +def ConnectToEIS(self, session_handle, app_id, options): + try: + logger.debug(f"ConnectToEIS({session_handle}, {app_id}, {options})") + + assert session_handle in self.sessions + + if self.fail_connect_to_eis: + raise dbus.exceptions.DBusException("Purposely failing ConnectToEIS") + + sockets = socket.socketpair() + self.eis_socket = sockets[0] + assert self.eis_socket.send(b"HELLO") == 5 + + return dbus.types.UnixFd(sockets[1]) + except Exception as e: + logger.critical(e) + raise e diff --git a/tests/test-doc-portal.c b/tests/test-doc-portal.c new file mode 100644 index 0000000..778a19d --- /dev/null +++ b/tests/test-doc-portal.c @@ -0,0 +1,879 @@ +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "document-portal/document-portal-dbus.h" + +#include "can-use-fuse.h" +#include "src/glib-backports.h" +#include "utils.h" + +char outdir[] = "/tmp/xdp-test-XXXXXX"; + +char fuse_status_file[] = "/tmp/test-xdp-fuse-XXXXXX"; + +GTestDBus *dbus; +GDBusConnection *session_bus; +XdpDbusDocuments *documents; +char *mountpoint; + +static gboolean +set_contents_trunc (const gchar *filename, + const gchar *contents, + gssize length, + GError **error) +{ + int fd; + + if (length == -1) + length = strlen (contents); + + fd = open (filename, O_RDWR | O_TRUNC | O_CREAT, 0644); + if (fd == -1) + { + int errsv = errno; + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errsv), + "Can't open %s", filename); + return FALSE; + } + + while (length > 0) + { + gssize s; + + s = write (fd, contents, length); + + if (s < 0) + { + int errsv = errno; + if (errsv == EINTR) + continue; + + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errsv), + "Can't write to %s", filename); + close (fd); + return FALSE; + } + + contents += s; + length -= s; + } + + close (fd); + return TRUE; +} + +static char * +make_doc_dir (const char *id, const char *app) +{ + if (app) + return g_build_filename (mountpoint, "by-app", app, id, NULL); + else + return g_build_filename (mountpoint, id, NULL); +} + +static char * +make_doc_path (const char *id, const char *basename, const char *app) +{ + g_autofree char *dir = make_doc_dir (id, app); + + return g_build_filename (dir, basename, NULL); +} + +static void +assert_host_has_contents (const char *basename, const char *expected_contents) +{ + g_autofree char *path = g_build_filename (outdir, basename, NULL); + g_autofree char *real_contents = NULL; + gsize real_contents_length; + GError *error = NULL; + + g_file_get_contents (path, &real_contents, &real_contents_length, &error); + g_assert_no_error (error); + g_assert_cmpstr (real_contents, ==, expected_contents); + g_assert_cmpuint (real_contents_length, ==, strlen (expected_contents)); +} + +static void +assert_doc_has_contents (const char *id, const char *basename, const char *app, const char *expected_contents) +{ + g_autofree char *path = make_doc_path (id, basename, app); + g_autofree char *real_contents = NULL; + gsize real_contents_length; + GError *error = NULL; + + g_file_get_contents (path, &real_contents, &real_contents_length, &error); + g_assert_no_error (error); + g_assert_cmpstr (real_contents, ==, expected_contents); + g_assert_cmpuint (real_contents_length, ==, strlen (expected_contents)); +} + +static void +assert_doc_not_exist (const char *id, const char *basename, const char *app) +{ + g_autofree char *path = make_doc_path (id, basename, app); + struct stat buf; + int res, fd; + + res = stat (path, &buf); + g_assert_cmpint (res, ==, -1); + g_assert_cmpint (errno, ==, ENOENT); + + fd = open (path, O_RDONLY); + g_assert_cmpint (fd, ==, -1); + g_assert_cmpint (errno, ==, ENOENT); +} + +static void +assert_doc_dir_not_exist (const char *id, const char *app) +{ + g_autofree char *path = make_doc_dir (id, app); + struct stat buf; + int res, fd; + + res = stat (path, &buf); + g_assert_cmpint (res, ==, -1); + g_assert_cmpint (errno, ==, ENOENT); + + fd = open (path, O_RDONLY); + g_assert_cmpint (fd, ==, -1); + g_assert_cmpint (errno, ==, ENOENT); +} + +static void +assert_doc_dir_exist (const char *id, const char *app) +{ + g_autofree char *path = make_doc_dir (id, app); + struct stat buf; + int res, fd; + + res = stat (path, &buf); + g_assert_cmpint (res, ==, 0); + + fd = open (path, O_RDONLY); + g_assert_cmpint (fd, !=, -1); + close (fd); +} + +static char * +export_named_file (const char *dir, const char *name, gboolean unique) +{ + int fd, fd_id; + GUnixFDList *fd_list = NULL; + + g_autoptr(GVariant) reply = NULL; + GError *error = NULL; + char *doc_id; + + fd = open (dir, O_PATH | O_CLOEXEC); + g_assert (fd >= 0); + + fd_list = g_unix_fd_list_new (); + fd_id = g_unix_fd_list_append (fd_list, fd, &error); + g_assert_no_error (error); + close (fd); + + reply = g_dbus_connection_call_with_unix_fd_list_sync (session_bus, + "org.freedesktop.portal.Documents", + "/org/freedesktop/portal/documents", + "org.freedesktop.portal.Documents", + "AddNamed", + g_variant_new ("(h^aybb)", fd_id, name, !unique, FALSE), + G_VARIANT_TYPE ("(s)"), + G_DBUS_CALL_FLAGS_NONE, + 30000, + fd_list, NULL, + NULL, + &error); + g_object_unref (fd_list); + g_assert_no_error (error); + g_assert (reply != NULL); + + g_variant_get (reply, "(s)", &doc_id); + g_assert (doc_id != NULL); + return doc_id; +} + +static char * +export_file (const char *path, gboolean unique) +{ + int fd, fd_id; + GUnixFDList *fd_list = NULL; + + g_autoptr(GVariant) reply = NULL; + GError *error = NULL; + char *doc_id; + + fd = open (path, O_PATH | O_CLOEXEC); + g_assert (fd >= 0); + + fd_list = g_unix_fd_list_new (); + fd_id = g_unix_fd_list_append (fd_list, fd, &error); + g_assert_no_error (error); + close (fd); + + reply = g_dbus_connection_call_with_unix_fd_list_sync (session_bus, + "org.freedesktop.portal.Documents", + "/org/freedesktop/portal/documents", + "org.freedesktop.portal.Documents", + "Add", + g_variant_new ("(hbb)", fd_id, !unique, FALSE), + G_VARIANT_TYPE ("(s)"), + G_DBUS_CALL_FLAGS_NONE, + 30000, + fd_list, NULL, + NULL, + &error); + g_object_unref (fd_list); + g_assert_no_error (error); + g_assert (reply != NULL); + + g_variant_get (reply, "(s)", &doc_id); + g_assert (doc_id != NULL); + return doc_id; +} + +static char * +export_new_file (const char *basename, const char *contents, gboolean unique) +{ + g_autofree char *path = NULL; + GError *error = NULL; + + path = g_build_filename (outdir, basename, NULL); + + g_file_set_contents (path, contents, -1, &error); + g_assert_no_error (error); + + return export_file (path, unique); +} + +static gboolean +update_doc_trunc (const char *id, const char *basename, const char *app, const char *contents, GError **error) +{ + g_autofree char *path = make_doc_path (id, basename, app); + + return set_contents_trunc (path, contents, -1, error); +} + +static gboolean +update_doc (const char *id, const char *basename, const char *app, const char *contents, GError **error) +{ + g_autofree char *path = make_doc_path (id, basename, app); + + return g_file_set_contents (path, contents, -1, error); +} + +static gboolean +update_from_host (const char *basename, const char *contents, GError **error) +{ + g_autofree char *path = g_build_filename (outdir, basename, NULL); + + return g_file_set_contents (path, contents, -1, error); +} + +static gboolean +unlink_doc (const char *id, const char *basename, const char *app, GError **error) +{ + g_autofree char *path = make_doc_path (id, basename, app); + + if (unlink (path) != 0) + { + int errsv = errno; + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errsv), + "Can't unlink %s", path); + return FALSE; + } + + return TRUE; +} + +static gboolean +unlink_doc_from_host (const char *basename, GError **error) +{ + g_autofree char *path = g_build_filename (outdir, basename, NULL); + + if (unlink (path) != 0) + { + int errsv = errno; + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errsv), + "Can't unlink %s", path); + return FALSE; + } + + return TRUE; +} + +static void +grant_permissions (const char *id, const char *app, gboolean write) +{ + g_autoptr(GPtrArray) permissions = g_ptr_array_new (); + GError *error = NULL; + + g_ptr_array_add (permissions, "read"); + if (write) + g_ptr_array_add (permissions, "write"); + g_ptr_array_add (permissions, NULL); + + xdp_dbus_documents_call_grant_permissions_sync (documents, + id, + app, + (const char **) permissions->pdata, + NULL, + &error); + g_assert_no_error (error); +} + +static void +test_create_doc (void) +{ + g_autofree char *doc_path = NULL; + g_autofree char *doc_app_path = NULL; + g_autofree char *host_path = NULL; + g_autofree char *id = NULL; + g_autofree char *id2 = NULL; + g_autofree char *id3 = NULL; + g_autofree char *id4 = NULL; + g_autofree char *id5 = NULL; + const char *basename = "a-file"; + GError *error = NULL; + + if (!check_fuse_or_skip_test ()) + return; + + /* Export a document */ + id = export_new_file (basename, "content", FALSE); + + /* Ensure its there and not viewable by apps */ + assert_doc_has_contents (id, basename, NULL, "content"); + assert_host_has_contents (basename, "content"); + assert_doc_not_exist (id, basename, "com.test.App1"); + assert_doc_not_exist (id, basename, "com.test.App2"); + assert_doc_not_exist (id, "another-file", NULL); + assert_doc_not_exist ("anotherid", basename, NULL); + + /* Create a tmp file in same dir, ensure it works and can't be seen by other apps */ + assert_doc_not_exist (id, "tmp1", NULL); + update_doc (id, "tmp1", NULL, "tmpdata1", &error); + g_assert_no_error (error); + assert_doc_has_contents (id, "tmp1", NULL, "tmpdata1"); + assert_doc_not_exist (id, "tmp1", "com.test.App1"); + + /* Let App 1 see the document (but not write) */ + grant_permissions (id, "com.test.App1", FALSE); + + /* Ensure App 1 and only it can see the document and tmpfile */ + assert_doc_has_contents (id, basename, "com.test.App1", "content"); + assert_doc_not_exist (id, basename, "com.test.App2"); + assert_doc_not_exist (id, "tmp1", "com.test.App1"); + + /* Make sure App 1 can't create a tmpfile */ + assert_doc_not_exist (id, "tmp2", "com.test.App1"); + update_doc (id, "tmp2", "com.test.App1", "tmpdata2", &error); + g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_ACCES); + g_clear_error (&error); + assert_doc_not_exist (id, "tmp2", "com.test.App1"); + + /* Update the document contents, ensure this is propagated */ + update_doc (id, basename, NULL, "content2", &error); + g_assert_no_error (error); + + assert_host_has_contents (basename, "content2"); + assert_doc_has_contents (id, basename, NULL, "content2"); + assert_doc_has_contents (id, basename, "com.test.App1", "content2"); + assert_doc_not_exist (id, basename, "com.test.App2"); + assert_doc_not_exist (id, "tmp1", "com.test.App2"); + + /* Update the document contents outside fuse fd, ensure this is propagated */ + update_from_host (basename, "content3", &error); + g_assert_no_error (error); + assert_host_has_contents (basename, "content3"); + assert_doc_has_contents (id, basename, NULL, "content3"); + assert_doc_has_contents (id, basename, "com.test.App1", "content3"); + assert_doc_not_exist (id, basename, "com.test.App2"); + assert_doc_not_exist (id, "tmp1", "com.test.App2"); + + /* Try to update the doc from an app that can't write to it */ + update_doc (id, basename, "com.test.App1", "content4", &error); + g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_ACCES); + g_clear_error (&error); + + /* Try to create a tmp file for an app that is not allowed */ + assert_doc_not_exist (id, "tmp2", "com.test.App1"); + update_doc (id, "tmp2", "com.test.App1", "tmpdata2", &error); + g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_ACCES); + g_clear_error (&error); + assert_doc_not_exist (id, "tmp2", "com.test.App1"); + assert_doc_not_exist (id, "tmp2", NULL); + + /* Grant write permissions to App1 */ + grant_permissions (id, "com.test.App1", TRUE); + + /* update the doc from an app with write access */ + update_doc (id, basename, "com.test.App1", "content5", &error); + g_assert_no_error (error); + assert_host_has_contents (basename, "content5"); + assert_doc_has_contents (id, basename, NULL, "content5"); + assert_doc_has_contents (id, basename, "com.test.App1", "content5"); + assert_doc_not_exist (id, basename, "com.test.App2"); + + /* Try to create a tmp file for an app */ + assert_doc_not_exist (id, "tmp3", "com.test.App1"); + update_doc (id, "tmp3", "com.test.App1", "tmpdata3", &error); + g_assert_no_error (error); + assert_doc_has_contents (id, "tmp3", "com.test.App1", "tmpdata3"); + assert_doc_not_exist (id, "tmp3", NULL); + + /* Re-Create a file from a fuse document file, in various ways */ + doc_path = make_doc_path (id, basename, NULL); + doc_app_path = make_doc_path (id, basename, "com.test.App1"); + host_path = g_build_filename (outdir, basename, NULL); + id2 = export_file (doc_path, FALSE); + g_assert_cmpstr (id, ==, id2); + id3 = export_file (doc_app_path, FALSE); + g_assert_cmpstr (id, ==, id3); + id4 = export_file (host_path, FALSE); + g_assert_cmpstr (id, ==, id4); + + /* Ensure we can make a unique document */ + id5 = export_file (host_path, TRUE); + g_assert_cmpstr (id, !=, id5); +} + +static void +test_recursive_doc (void) +{ + g_autofree char *id = NULL; + g_autofree char *id2 = NULL; + g_autofree char *id3 = NULL; + const char *basename = "recursive-file"; + g_autofree char *path = NULL; + g_autofree char *app_path = NULL; + + if (!check_fuse_or_skip_test ()) + return; + + id = export_new_file (basename, "recursive-content", FALSE); + + assert_doc_has_contents (id, basename, NULL, "recursive-content"); + + path = make_doc_path (id, basename, NULL); + g_debug ("path: %s\n", path); + + id2 = export_file (path, FALSE); + + g_assert_cmpstr (id, ==, id2); + + grant_permissions (id, "com.test.App1", FALSE); + + app_path = make_doc_path (id, basename, "com.test.App1"); + + id3 = export_file (app_path, FALSE); + + g_assert_cmpstr (id, ==, id3); +} + +static void +test_create_docs (void) +{ + GError *error = NULL; + g_autofree char *path1 = NULL; + g_autofree char *path2 = NULL; + int fd1, fd2; + guint32 fd_ids[2]; + GUnixFDList *fd_list = NULL; + gboolean res; + g_auto(GStrv) out_doc_ids = NULL; + g_autoptr(GVariant) out_extra = NULL; + const char *permissions[] = { "read", NULL }; + const char *basenames[] = { "doc1", "doc2" }; + int i; + + if (!check_fuse_or_skip_test ()) + return; + + path1 = g_build_filename (outdir, basenames[0], NULL); + g_file_set_contents (path1, basenames[0], -1, &error); + g_assert_no_error (error); + + fd1 = open (path1, O_PATH | O_CLOEXEC); + g_assert (fd1 >= 0); + + path2 = g_build_filename (outdir, basenames[1], NULL); + g_file_set_contents (path2, basenames[1], -1, &error); + g_assert_no_error (error); + + fd2 = open (path2, O_PATH | O_CLOEXEC); + g_assert (fd2 >= 0); + + fd_list = g_unix_fd_list_new (); + fd_ids[0] = g_unix_fd_list_append (fd_list, fd1, &error); + g_assert_no_error (error); + close (fd1); + fd_ids[1] = g_unix_fd_list_append (fd_list, fd2, &error); + g_assert_no_error (error); + close (fd2); + + res = xdp_dbus_documents_call_add_full_sync (documents, + g_variant_new_fixed_array (G_VARIANT_TYPE_HANDLE, + fd_ids, 2, sizeof (guint32)), + 0, + "org.other.App", + permissions, + fd_list, + &out_doc_ids, + &out_extra, + NULL, + NULL, &error); + g_assert_no_error (error); + g_assert (res); + + g_assert (g_strv_length (out_doc_ids) == 2); + for (i = 0; i < 2; i++) + { + const char *id = out_doc_ids[i]; + + /* Ensure its there and not viewable by apps */ + assert_doc_has_contents (id, basenames[i], NULL, basenames[i]); + assert_host_has_contents (basenames[i], basenames[i]); + assert_doc_not_exist (id, basenames[i], "com.test.App1"); + assert_doc_not_exist (id, basenames[i], "com.test.App2"); + assert_doc_not_exist (id, "another-file", NULL); + assert_doc_not_exist ("anotherid", basenames[i], NULL); + + assert_doc_has_contents (id, basenames[i], "org.other.App", basenames[i]); + update_doc (id, basenames[i], "org.other.App", "tmpdata2", &error); + g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_ACCES); + g_clear_error (&error); + } + g_assert (g_variant_lookup_value (out_extra, "mountpoint", G_VARIANT_TYPE_VARIANT) == 0); +} + + +static void +test_add_named (void) +{ + g_autofree char *id1 = NULL; + const char *basename1 = "add-named-1"; + GError *error = NULL; + gboolean res; + + if (!check_fuse_or_skip_test ()) + return; + + id1 = export_named_file (outdir, basename1, FALSE); + + assert_doc_dir_exist (id1, NULL); + assert_doc_dir_not_exist (id1, "com.test.App1"); + assert_doc_not_exist (id1, basename1, NULL); + assert_doc_not_exist (id1, basename1, "com.test.App1"); + + grant_permissions (id1, "com.test.App1", TRUE); + + assert_doc_dir_exist (id1, NULL); + assert_doc_dir_exist (id1, "com.test.App1"); + assert_doc_not_exist (id1, basename1, NULL); + assert_doc_not_exist (id1, basename1, "com.test.App1"); + + /* Update truncating with no previous file */ + res = update_doc_trunc (id1, basename1, NULL, "foobar", &error); + g_assert_no_error (error); + g_assert (res == TRUE); + + assert_doc_has_contents (id1, basename1, NULL, "foobar"); + assert_doc_has_contents (id1, basename1, "com.test.App1", "foobar"); + assert_doc_not_exist (id1, basename1, "com.test.App2"); + + /* Update truncating with previous file */ + res = update_doc_trunc (id1, basename1, NULL, "foobar2", &error); + g_assert_no_error (error); + g_assert (res == TRUE); + + assert_doc_has_contents (id1, basename1, NULL, "foobar2"); + assert_doc_has_contents (id1, basename1, "com.test.App1", "foobar2"); + assert_doc_not_exist (id1, basename1, "com.test.App2"); + + /* Update atomic with previous file */ + res = update_doc (id1, basename1, NULL, "foobar3", &error); + g_assert_no_error (error); + g_assert (res == TRUE); + + assert_doc_has_contents (id1, basename1, NULL, "foobar3"); + assert_doc_has_contents (id1, basename1, "com.test.App1", "foobar3"); + assert_doc_not_exist (id1, basename1, "com.test.App2"); + + /* Update from host */ + res = update_from_host (basename1, "foobar4", &error); + g_assert_no_error (error); + g_assert (res == TRUE); + + assert_doc_has_contents (id1, basename1, NULL, "foobar4"); + assert_doc_has_contents (id1, basename1, "com.test.App1", "foobar4"); + assert_doc_not_exist (id1, basename1, "com.test.App2"); + + /* Unlink doc */ + res = unlink_doc (id1, basename1, NULL, &error); + g_assert_no_error (error); + g_assert (res == TRUE); + + assert_doc_dir_exist (id1, NULL); + assert_doc_dir_exist (id1, "com.test.App1"); + assert_doc_not_exist (id1, basename1, NULL); + assert_doc_not_exist (id1, basename1, "com.test.App1"); + + /* Update atomic with no previous file */ + res = update_doc (id1, basename1, NULL, "foobar5", &error); + g_assert_no_error (error); + g_assert (res == TRUE); + + assert_doc_has_contents (id1, basename1, NULL, "foobar5"); + assert_doc_has_contents (id1, basename1, "com.test.App1", "foobar5"); + assert_doc_not_exist (id1, basename1, "com.test.App2"); + + /* Unlink doc on host */ + res = unlink_doc_from_host (basename1, &error); + g_assert_no_error (error); + g_assert (res == TRUE); + + assert_doc_dir_exist (id1, NULL); + assert_doc_dir_exist (id1, "com.test.App1"); + assert_doc_not_exist (id1, basename1, NULL); + assert_doc_not_exist (id1, basename1, "com.test.App1"); + + /* Update atomic with unexpected no previous file */ + res = update_doc (id1, basename1, NULL, "foobar6", &error); + g_assert_no_error (error); + g_assert (res == TRUE); + + assert_doc_has_contents (id1, basename1, NULL, "foobar6"); + assert_doc_has_contents (id1, basename1, "com.test.App1", "foobar6"); + assert_doc_not_exist (id1, basename1, "com.test.App2"); + + /* Unlink doc on host again */ + res = unlink_doc_from_host (basename1, &error); + g_assert_no_error (error); + g_assert (res == TRUE); + + assert_doc_dir_exist (id1, NULL); + assert_doc_dir_exist (id1, "com.test.App1"); + assert_doc_not_exist (id1, basename1, NULL); + assert_doc_not_exist (id1, basename1, "com.test.App1"); + + /* Update truncating with unexpected no previous file */ + res = update_doc_trunc (id1, basename1, NULL, "foobar7", &error); + g_assert_no_error (error); + g_assert (res == TRUE); + + assert_doc_has_contents (id1, basename1, NULL, "foobar7"); + assert_doc_has_contents (id1, basename1, "com.test.App1", "foobar7"); + assert_doc_not_exist (id1, basename1, "com.test.App2"); +} + +static void +global_setup (void) +{ + gboolean inited; + GError *error = NULL; + g_autofree gchar *services = NULL; + int fd; + + if (!check_fuse ()) + { + g_assert_cmpstr (cannot_use_fuse, !=, NULL); + return; + } + + g_log_writer_default_set_use_stderr (TRUE); + + g_mkdtemp (outdir); + g_debug ("outdir: %s\n", outdir); + + fd = g_mkstemp (fuse_status_file); + close (fd); + + g_setenv ("XDG_RUNTIME_DIR", outdir, TRUE); + g_setenv ("XDG_DATA_HOME", outdir, TRUE); + g_setenv ("TEST_DOCUMENT_PORTAL_FUSE_STATUS", fuse_status_file, TRUE); + + /* Re-defining dbus-monitor with a custom script */ + setup_dbus_daemon_wrapper (outdir); + + dbus = g_test_dbus_new (G_TEST_DBUS_NONE); + services = g_test_build_filename (G_TEST_BUILT, "services", NULL); + g_test_dbus_add_service_dir (dbus, services); + g_test_dbus_up (dbus); + + /* g_test_dbus_up unsets this, so re-set */ + g_setenv ("XDG_RUNTIME_DIR", outdir, TRUE); + + session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); + g_assert_no_error (error); + + documents = xdp_dbus_documents_proxy_new_sync (session_bus, 0, + "org.freedesktop.portal.Documents", + "/org/freedesktop/portal/documents", + NULL, &error); + g_assert_no_error (error); + g_assert (documents != NULL); + + inited = xdp_dbus_documents_call_get_mount_point_sync (documents, &mountpoint, + NULL, &error); + g_assert_no_error (error); + g_assert (inited); + g_assert (mountpoint != NULL); +} + +static gboolean +rm_rf_dir (GFile *dir, + GError **error) +{ + GFileEnumerator *enumerator = NULL; + g_autoptr(GFileInfo) child_info = NULL; + GError *temp_error = NULL; + + enumerator = g_file_enumerate_children (dir, "standard::type,standard::name", + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + NULL, error); + if (!enumerator) + return FALSE; + + while ((child_info = g_file_enumerator_next_file (enumerator, NULL, &temp_error))) + { + const char *name = g_file_info_get_name (child_info); + g_autoptr(GFile) child = g_file_get_child (dir, name); + + if (g_file_info_get_file_type (child_info) == G_FILE_TYPE_DIRECTORY) + { + if (!rm_rf_dir (child, error)) + return FALSE; + } + else + { + if (!g_file_delete (child, NULL, error)) + return FALSE; + } + + g_clear_object (&child_info); + } + + if (temp_error != NULL) + { + g_propagate_error (error, temp_error); + return FALSE; + } + + if (!g_file_delete (dir, NULL, error)) + return FALSE; + + return TRUE; +} + + +static void +global_teardown (void) +{ + GError *error = NULL; + char *argv[] = { "fusermount3", "-u", NULL, NULL }; + g_autofree char *by_app_dir = g_build_filename (mountpoint, "by-app", NULL); + struct stat buf; + g_autoptr(GFile) outdir_file = g_file_new_for_path (outdir); + int res, i; + + if (cannot_use_fuse != NULL) + return; + + res = stat (by_app_dir, &buf); + g_assert_cmpint (res, ==, 0); + + argv[2] = mountpoint; + + g_spawn_sync (NULL, argv, NULL, G_SPAWN_SEARCH_PATH, + NULL, NULL, NULL, NULL, NULL, &error); + g_assert_no_error (error); + + res = stat (by_app_dir, &buf); + g_assert_cmpint (res, ==, -1); + g_assert_cmpint (errno, ==, ENOENT); + + for (i = 0; i < 1000; i++) + { + g_autofree char *fuse_unmount_status = NULL; + + g_file_get_contents (fuse_status_file, &fuse_unmount_status, NULL, &error); + g_assert_no_error (error); + /* Loop until something is written to the status file */ + if (strlen (fuse_unmount_status) > 0) + { + g_assert_cmpstr (fuse_unmount_status, ==, "ok"); + break; + } + g_usleep (G_USEC_PER_SEC / 100); + } + g_assert (i != 1000); /* We timed out before writing to the status file */ + (void) unlink (fuse_status_file); + + g_free (mountpoint); + + g_object_unref (documents); + + g_dbus_connection_close_sync (session_bus, NULL, &error); + g_assert_no_error (error); + + g_object_unref (session_bus); + + g_test_dbus_down (dbus); + + g_object_unref (dbus); + + res = rm_rf_dir (outdir_file, &error); + g_assert_no_error (error); +} + +static void +test_version (void) +{ + if (!check_fuse_or_skip_test ()) + return; + + g_assert_cmpint (xdp_dbus_documents_get_version (documents), ==, 4); +} + +int +main (int argc, char **argv) +{ + int res; + + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/db/version", test_version); + g_test_add_func ("/db/create_doc", test_create_doc); + g_test_add_func ("/db/recursive_doc", test_recursive_doc); + g_test_add_func ("/db/create_docs", test_create_docs); + g_test_add_func ("/db/add_named", test_add_named); + + global_setup (); + + res = g_test_run (); + + global_teardown (); + + return res; +} diff --git a/tests/test-document-fuse.py b/tests/test-document-fuse.py new file mode 100755 index 0000000..9e34f59 --- /dev/null +++ b/tests/test-document-fuse.py @@ -0,0 +1,1067 @@ +#!/usr/bin/env python3 + +import argparse +import errno +import os +import random +import stat +import sys + +from gi.repository import Gio, GLib + + +def filename_to_ay(filename): + return list(filename.encode("utf-8")) + [0] + + +running_count = {} + +app_prefix = "org.test." +dir_prefix = "dir" +ensure_no_remaining = True + +parser = argparse.ArgumentParser() +parser.add_argument("--verbose", "-v", action="count") +parser.add_argument("--iterations", type=int, default=3) +parser.add_argument("--prefix") +args = parser.parse_args(sys.argv[1:]) + +if args.prefix: + app_prefix = app_prefix + args.prefix + "." + dir_prefix = dir_prefix + "-" + args.prefix + "-" + ensure_no_remaining = False + + +def log(str): + if args.prefix: + print("%s: %s" % (args.prefix, str)) + else: + print(str) + + +def logv(str): + if args.verbose: + log(str) + + +def get_a_count(counter): + global running_count + if counter in running_count: + count = running_count[counter] + count = count + 1 + running_count[counter] = count + return count + running_count[counter] = 1 + return 1 + + +def setFileContent(path, content): + with open(path, "w") as f: + f.write(content) + + +def appendFileContent(path, content): + with open(path, "a") as f: + f.write(content) + + +def readFdContent(fd): + os.lseek(fd, 0, os.SEEK_SET) + return str(os.read(fd, 64 * 1024), "utf-8") + + +def replaceFdContent(fd, content): + os.lseek(fd, 0, os.SEEK_SET) + os.ftruncate(fd, 0) + os.write(fd, bytes(content, "utf-8")) + + +def appendFdContent(fd, content): + os.lseek(fd, 0, os.SEEK_END) + os.write(fd, bytes(content, "utf-8")) + + +TEST_DATA_DIR = os.environ["TEST_DATA_DIR"] +DOCUMENT_ADD_FLAGS_REUSE_EXISTING = 1 << 0 +DOCUMENT_ADD_FLAGS_PERSISTENT = 1 << 1 +DOCUMENT_ADD_FLAGS_AS_NEEDED_BY_APP = 1 << 2 +DOCUMENT_ADD_FLAGS_DIRECTORY = 1 << 3 + + +def assertRaises(exc_type, func, *args, **kwargs): + raised_exc = None + try: + func(*args, **kwargs) + except: + raised_exc = sys.exc_info()[0] + + if not raised_exc: + raise AssertionError("{0} was not raised".format(exc_type.__name__)) + if raised_exc != exc_type: + raise AssertionError( + "Wrong assertion type {0} was raised instead of {1}".format( + raised_exc.__name__, exc_type.__name__ + ) + ) + + +def assertRaisesErrno(error_nr, func, *args, **kwargs): + raised_exc = None + raised_exc_value = None + try: + func(*args, **kwargs) + except: + raised_exc = sys.exc_info()[0] + raised_exc_value = sys.exc_info()[1] + + if not raised_exc: + raise AssertionError("No assertion was raised") + if raised_exc != OSError: + raise AssertionError("OSError was not raised") + if raised_exc_value.errno != error_nr: + raise AssertionError( + "Wrong errno {0} was raised instead of {1}".format( + raised_exc_value.errno, error_nr + ) + ) + + +def assertFileHasContent(path, expected_content): + with open(path) as f: + file_content = f.read() + assert file_content == expected_content + + +def assertFdHasContent(fd, expected_content): + content = readFdContent(fd) + assert content == expected_content + + +def assertSameStat(a, b, b_mode_mask): + if not ( + a.st_mode == (b.st_mode & b_mode_mask) + and a.st_nlink == b.st_nlink + and a.st_size == b.st_size + and a.st_uid == b.st_uid + and a.st_gid == b.st_gid + and a.st_atime == b.st_atime + and a.st_mtime == b.st_mtime + and a.st_ctime == b.st_ctime + ): + raise AssertionError("Stat value {} was not the expected {})".format(a, b)) + + +def assertFileExist(path): + try: + info = os.lstat(path) + if info.st_mode & stat.S_IFREG != stat.S_IFREG: + raise AssertionError("File {} is not a regular file".format(path)) + except: + raise AssertionError("File {} doesn't exist".format(path)) + + +def assertDirExist(path): + try: + info = os.lstat(path) + if info.st_mode & stat.S_IFDIR != stat.S_IFDIR: + raise AssertionError("File {} is not a directory file".format(path)) + except: + raise AssertionError("File {} doesn't exist".format(path)) + + +def assertSymlink(path, expected_target): + try: + info = os.lstat(path) + if info.st_mode & stat.S_IFLNK != stat.S_IFLNK: + raise AssertionError("File {} is not a symlink".format(path)) + target = os.readlink(path) + if target != expected_target: + raise AssertionError( + "File {} has wrong target {}, expected {}".format( + path, target, expected_target + ) + ) + except: + raise AssertionError("Symlink {} doesn't exist".format(path)) + + +def assertFileNotExist(path): + try: + os.lstat(path) + except FileNotFoundError: + return + except: + raise AssertionError( + "Got wrong execption {} for {}, expected FileNotFoundError".format( + sys.exc_info()[0], path + ) + ) + raise AssertionError("Path {} unexpectedly exists".format(path)) + + +def assertDirFiles(path, expected_files, exhaustive=True, volatile_files=None): + found_files = os.listdir(path) + remaining = set(found_files) + for file in expected_files: + if file in remaining: + remaining.remove(file) + elif file not in volatile_files: + raise AssertionError( + "Expected file {} not found in dir {} (all: {})".format( + file, path, found_files + ) + ) + if exhaustive: + if len(remaining) != 0: + raise AssertionError( + "Unexpected files {} in dir {} (all: {})".format( + remaining, path, found_files + ) + ) + + +class Doc: + def __init__(self, portal, id, path, content, is_dir=False): + self.portal = portal + self.id = id + self.content = content + self.real_path = path + self.is_dir = is_dir + self.apps = [] + self.files = [] + + if is_dir: + self.real_dirname = path + self.filename = None + self.dirname = os.path.basename(path) + else: + (self.real_dirname, self.filename) = os.path.split(path) + self.dirname = None + if content: + self.files.append(self.filename) + + def is_readable_by(self, app_id): + if app_id: + return app_id in self.apps + return True + + def is_writable_by(self, app_id): + if app_id: + return app_id in self.apps and ".write." in app_id + else: + return True + + def get_doc_path(self, app_id): + if app_id: + base = portal.app_path(app_id) + "/" + self.id + else: + base = portal.mountpoint + "/" + self.id + if self.is_dir: + return base + "/" + self.dirname + else: + return base + + def __str__(self): + name = self.id + if self.is_dir: + return "%s(dir)" % (name) + elif self.content is None: + return "%s(missing)" % (name) + else: + return "%s" % (name) + + +class DocPortal: + def __init__(self): + self.apps = [] + self.volatile_apps = set() + self.docs = {} + self.bus = Gio.bus_get_sync(Gio.BusType.SESSION, None) + self.proxy = Gio.DBusProxy.new_sync( + self.bus, + Gio.DBusProxyFlags.NONE, + None, + "org.freedesktop.portal.Documents", + "/org/freedesktop/portal/documents", + "org.freedesktop.portal.Documents", + None, + ) + self.mountpoint = self.get_mount_path() + + def get_mount_path(self): + res = self.proxy.call_sync("GetMountPoint", GLib.Variant("()", ()), 0, -1, None) + return bytearray(res[0][:-1]).decode("utf-8") + + def grant_permissions(self, doc_id, app_id, permissions): + self.proxy.call_sync( + "GrantPermissions", + GLib.Variant("(ssas)", (doc_id, app_id, permissions)), + 0, + -1, + None, + ) + + def lookup(self, path): + res = self.proxy.call_sync( + "Lookup", GLib.Variant("(ay)", (filename_to_ay(path),)), 0, -1, None + ) + return res[0] + + def delete(self, doc_id): + self.proxy.call_sync("Delete", GLib.Variant("(s)", (doc_id,)), 0, -1, None) + del self.docs[doc_id] + + def add(self, path, reuse_existing=True): + fdlist = Gio.UnixFDList.new() + fd = os.open(path, os.O_PATH) + handle = fdlist.append(fd) + os.close(fd) + res = self.proxy.call_with_unix_fd_list_sync( + "Add", + GLib.Variant("(hbb)", (handle, reuse_existing, False)), + 0, + -1, + fdlist, + None, + ) + doc_id = res[0][0] + if doc_id in self.docs: + return self.docs[doc_id] + + with open(path) as f: + content = f.read() + doc = Doc(self, doc_id, path, content) + self.docs[doc.id] = doc + return doc + + def add_named(self, path, reuse_existing=True): + (dirname, filename) = os.path.split(path) + fdlist = Gio.UnixFDList.new() + fd = os.open(dirname, os.O_PATH) + handle = fdlist.append(fd) + os.close(fd) + res = self.proxy.call_with_unix_fd_list_sync( + "AddNamed", + GLib.Variant( + "(haybb)", (handle, filename_to_ay(filename), reuse_existing, False) + ), + 0, + -1, + fdlist, + None, + ) + doc_id = res[0][0] + if doc_id in self.docs: + return self.docs[doc_id] + + try: + with open(path) as f: + content = f.read() + except: + content = None + doc = Doc(self, doc_id, path, content) + self.docs[doc.id] = doc + return doc + + def add_full(self, path, flags): + fdlist = Gio.UnixFDList.new() + fd = os.open(path, os.O_PATH) + handle = fdlist.append(fd) + os.close(fd) + res = self.proxy.call_with_unix_fd_list_sync( + "AddFull", + GLib.Variant("(ahusas)", ([handle], flags, "", [])), + 0, + -1, + fdlist, + None, + ) + doc_id = res[0][0][0] + if doc_id in self.docs: + return self.docs[doc_id] + doc = Doc(self, doc_id, path, True, (flags & DOCUMENT_ADD_FLAGS_DIRECTORY) != 0) + self.docs[doc.id] = doc + return doc + + def add_dir(self, path): + return self.add_full( + path, DOCUMENT_ADD_FLAGS_REUSE_EXISTING | DOCUMENT_ADD_FLAGS_DIRECTORY + ) + + def get_docs_for_app(self, app_id): + docs = [] + for doc in self.docs.values(): + if doc.is_readable_by(app_id): + docs.append(doc.id) + return docs + + def ensure_app_id(self, app_id, volatile=False): + if app_id not in self.apps: + self.apps.append(app_id) + if volatile: + self.volatile_apps.add(app_id) + + def get_docs(self): + return list(portal.docs.values()) + + def get_docs_randomized(self): + docs = list(portal.docs.values()) + random.shuffle(docs) + return docs + + def get_doc(self, doc_id): + return self.docs[doc_id] + + def get_app_ids(self): + return self.apps + + def get_volatile_app_ids(self): + return self.volatile_apps + + def get_app_ids_randomized(self): + apps = self.apps.copy() + random.shuffle(apps) + return apps + + def by_app_path(self): + return portal.mountpoint + "/by-app" + + def app_path(self, app_id): + return portal.mountpoint + "/by-app/" + app_id + + +def check_virtual_stat(info, writable=False): + assert info.st_uid == os.getuid() + assert info.st_gid == os.getgid() + if writable: + assert info.st_mode == stat.S_IFDIR | 0o700 + else: + assert info.st_mode == stat.S_IFDIR | 0o500 + + +def verify_virtual_dir(path, files, volatile_files=None): + info = os.lstat(path) + check_virtual_stat(info) + assert os.access(path, os.R_OK) + assert not os.access(path, os.W_OK) + + assertRaises(FileNotFoundError, os.lstat, path + "/not-existing-file") + + if files is not None: + assertDirFiles(path, files, ensure_no_remaining, volatile_files) + + +def verify_doc(doc, app_id=None): + dir = doc.get_doc_path(app_id) + + if doc.is_dir: + vdir = os.path.dirname(dir) + info = os.lstat(vdir) + check_virtual_stat(info) + pass + else: + info = os.lstat(dir) + check_virtual_stat(info, doc.is_writable_by(app_id)) + assert os.access(dir, os.R_OK) + if doc.is_writable_by(app_id): + assert os.access(dir, os.W_OK) + else: + assert not os.access(dir, os.W_OK) + + assertRaises(FileNotFoundError, os.lstat, dir + "/not-existing-file") + + assertDirFiles(dir, doc.files) + + for file in doc.files: + filepath = dir + "/" + file + info = os.lstat(filepath) + assert info.st_uid == os.getuid() + assert info.st_gid == os.getgid() + + assert os.access(filepath, os.R_OK) + if doc.is_writable_by(app_id): + assert os.access(filepath, os.W_OK) + else: + assert not os.access(filepath, os.W_OK) + + if doc.filename: + main_path = dir + "/" + doc.filename + real_path = doc.real_path + if doc.content: + assertFileExist(main_path) + assertFileHasContent(main_path, doc.content) + assertFileHasContent(real_path, doc.content) + + info = os.lstat(main_path) + real_info = os.lstat(real_path) + mode_mask = ~(stat.S_ISUID | stat.S_ISGID | stat.S_ISVTX) + if not doc.is_writable_by(app_id): + mode_mask = mode_mask & ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH) + assertSameStat(info, real_info, mode_mask) + + else: + assertRaises(FileNotFoundError, os.lstat, main_path) + assertRaises(FileNotFoundError, os.open, main_path, os.O_RDONLY) + assertRaises(FileNotFoundError, os.lstat, doc.real_path) + assertRaises(FileNotFoundError, os.open, doc.real_path, os.O_RDONLY) + + # Ensure no leftover temp files + for real_file in os.listdir(os.path.dirname(doc.real_path)): + assert not real_file.startswith(".xdp") + + +def verify_fs_layout(): + verify_virtual_dir(portal.mountpoint, ["by-app"] + list(portal.docs.keys())) + verify_virtual_dir( + portal.by_app_path(), portal.get_app_ids(), portal.get_volatile_app_ids() + ) + + for doc in portal.get_docs(): + verify_doc(doc) + + # Verify the by-app subdirs (just the directory for now) + for app_id in portal.get_app_ids(): + docs_for_app = portal.get_docs_for_app(app_id) + verify_virtual_dir(portal.app_path(app_id), docs_for_app) + for doc_id in docs_for_app: + doc = portal.get_doc(doc_id) + verify_doc(doc, app_id) + + +def check_virtdir_perms(path): + assertRaises(PermissionError, os.mkdir, path + "/a_dir") + assertRaises(PermissionError, os.open, path + "/a-file", os.O_RDWR | os.O_CREAT) + + +def check_root_perms(path): + check_virtdir_perms(path) + assertRaises(PermissionError, os.rename, path + "/by-app", path + "/by-app2") + assertRaises(PermissionError, os.rmdir, path + "/by-app") + + +def check_byapp_perms(path): + check_virtdir_perms(path) + assertRaises(PermissionError, os.mkdir, path + "/a_dir") + + +def check_regular_doc_perms(doc, app_id): + path = doc.get_doc_path(app_id) + writable = doc.is_writable_by(app_id) + # regular documents, can't do most stuff + assertRaises(PermissionError, os.mkdir, path + "/dir") + assertRaises(PermissionError, os.symlink, "symlink-value", path + "/symlink") + + docpath = path + "/" + doc.filename + tmppath = path + "/a-tmpfile" + tmppath2 = path + "/another-tmpfile" + if doc.content: # Main file exists + assertFileExist(docpath) + assertFileExist(doc.real_path) + assertRaises(PermissionError, os.link, docpath, path + "/a-hardlink") + assertRaises(NotADirectoryError, os.rmdir, docpath) + assertRaises(PermissionError, os.setxattr, docpath, "user.attr", b"foo") + assertRaises(PermissionError, os.removexattr, docpath, "user.attr") + + fd = os.open(docpath, os.O_RDONLY, 0o600) + os.close(fd) + + if not writable: + assertRaises( + PermissionError, os.open, docpath, os.O_RDONLY | os.O_TRUNC, 0o600 + ) + assertRaises(PermissionError, os.open, docpath, os.O_WRONLY, 0o600) + assertRaises(PermissionError, os.open, docpath, os.O_RDWR, 0o600) + assertRaises(PermissionError, os.rename, docpath, docpath + "renamed") + assertRaises(PermissionError, os.truncate, docpath, 1) + assertRaises(PermissionError, os.unlink, docpath) + assertRaises(PermissionError, os.utime, docpath) + else: + # Can't move file out of docdir or into other version of same docdir + assertRaisesErrno( + errno.EXDEV, os.rename, docpath, path + "/../" + doc.filename + ) + if app_id: + assertRaisesErrno( + errno.EXDEV, + os.rename, + docpath, + doc.get_doc_path(None) + doc.filename, + ) + if doc.apps and app_id != doc.apps[0]: + assertRaisesErrno( + errno.EXDEV, + os.rename, + docpath, + doc.get_doc_path(doc.apps[0]) + doc.filename, + ) + + # Ensure we can read it (multiple times) + fd = os.open(docpath, os.O_RDONLY, 0o600) + assertFdHasContent(fd, doc.content) + assertFdHasContent(fd, doc.content) + + # Ensure we can rename it + os.rename(docpath, docpath + "_renamed") + assertRaises(FileNotFoundError, os.open, docpath, os.O_RDONLY, 0o600) + # ... and still read it + assertFdHasContent(fd, doc.content) + + # Ensure we can delete it + os.unlink(docpath + "_renamed") + # ... and still read it + assertFdHasContent(fd, doc.content) + os.close(fd) + + # Replace main file with rename of tmpfile + setFileContent(docpath, "orig-data") + fd1 = os.open(docpath, os.O_RDONLY, 0o600) + + setFileContent(tmppath, "new-data") + fd2 = os.open(tmppath, os.O_RDONLY, 0o600) + + os.rename(tmppath, tmppath2) + assertRaises(FileNotFoundError, os.lstat, tmppath) + assertFdHasContent(fd2, "new-data") + assertFileHasContent(tmppath2, "new-data") + + os.rename(tmppath2, docpath) + assertRaises(FileNotFoundError, os.lstat, tmppath2) + assertFdHasContent(fd1, "orig-data") + assertFdHasContent(fd2, "new-data") + assertFileHasContent(docpath, "new-data") + appendFileContent(docpath, "-more") + assertFdHasContent(fd2, "new-data-more") + + setFileContent(tmppath, "replace-this-data") + fd3 = os.open(tmppath, os.O_RDONLY, 0o600) + os.rename(docpath, tmppath) + assertFdHasContent(fd2, "new-data-more") + assertFdHasContent(fd3, "replace-this-data") + fd4 = os.open(tmppath, os.O_RDWR, 0o600) + assertFdHasContent(fd4, "new-data-more") + + # Restore original version + os.rename(tmppath, docpath) + replaceFdContent(fd4, doc.content) + assertFdHasContent(fd2, doc.content) + assertFdHasContent(fd4, doc.content) + assertFileHasContent(docpath, doc.content) + assertFdHasContent(fd1, "orig-data") + assertFdHasContent(fd3, "replace-this-data") + + os.close(fd1) + os.close(fd2) + os.close(fd3) + os.close(fd4) + + assertRaises(NotADirectoryError, os.rmdir, docpath) + assertRaises(PermissionError, os.link, docpath, path + "/a-hardlink") + assertRaises(PermissionError, os.setxattr, docpath, "user.attr", b"foo") + assertRaises(PermissionError, os.removexattr, docpath, "user.attr") + + else: # Main file doesn't exist + assertFileNotExist(docpath) + assertFileNotExist(doc.real_path) + if writable: # But we can create it + setFileContent(docpath, "some-data") + assertFileHasContent(docpath, "some-data") + os.unlink(docpath) + else: # And we can't create it + assertRaises( + PermissionError, + os.open, + docpath, + os.O_CREAT | os.O_RDONLY | os.O_TRUNC, + 0o600, + ) + assertRaises( + PermissionError, os.open, docpath, os.O_CREAT | os.O_WRONLY, 0o600 + ) + assertRaises( + PermissionError, os.open, docpath, os.O_CREAT | os.O_RDWR, 0o600 + ) + + # Ensure it show up if created outside + setFileContent(doc.real_path, "from-outside") + assertFileExist(docpath) + assertFileHasContent(docpath, "from-outside") + if writable: + os.unlink(docpath) + else: + assertRaises(PermissionError, os.unlink, docpath) + os.unlink(doc.real_path) + assertFileNotExist(docpath) + + if writable: # We can create tempfiles, do some simple checks + setFileContent(tmppath, "tempdata") + assertFileHasContent(tmppath, "tempdata") + assertRaises(NotADirectoryError, os.rmdir, tmppath) + assertRaises(PermissionError, os.link, tmppath, path + "/a-hardlink") + assertRaises(PermissionError, os.setxattr, tmppath, "user.attr", b"foo") + assertRaises(PermissionError, os.removexattr, tmppath, "user.attr") + + os.rename(tmppath, tmppath2) + assertFileHasContent(tmppath2, "tempdata") + os.unlink(tmppath2) + else: + # We should be unable to create tempfiles + assertRaises( + PermissionError, + os.open, + tmppath, + os.O_CREAT | os.O_RDONLY | os.O_TRUNC, + 0o600, + ) + assertRaises(PermissionError, os.open, tmppath, os.O_CREAT | os.O_WRONLY, 0o600) + assertRaises(PermissionError, os.open, tmppath, os.O_CREAT | os.O_RDWR, 0o600) + + +def check_directory_doc_perms(doc, app_id): + writable = doc.is_writable_by(app_id) + + docpath = doc.get_doc_path(app_id) + realpath = doc.real_path + + # We should not be able to do anything with the toplevel document dir (other than reading the real dir) + vpath = os.path.dirname(docpath) + assertDirExist(vpath) + assertDirFiles(vpath, [doc.dirname]) + assertRaises(PermissionError, os.mkdir, vpath + "/a_dir") + assertRaises(PermissionError, os.rename, docpath, vpath + "/foo") + assertRaises(PermissionError, os.rmdir, docpath) + assertRaises( + PermissionError, os.open, vpath + "/a_file", os.O_CREAT | os.O_RDWR, 0o600 + ) + + assertDirExist(docpath) + + # Create some pre-existing files: + + real_dir = realpath + "/dir" + os.mkdir(real_dir) + setFileContent(real_dir + "/realfile", "real1") + setFileContent(real_dir + "/readonly", "readonly") + os.chmod(real_dir + "/readonly", 0o500) + os.mkdir(real_dir + "/subdir") + os.link(real_dir + "/realfile", real_dir + "/subdir/hardlink") + os.symlink("realfile", real_dir + "/symlink") + os.symlink("the-void", real_dir + "/broken-symlink") + + # Ensure they are visible via portal + + dir = docpath + "/dir" + assertDirFiles(docpath, ["dir"]) + assertDirExist(dir) + assertDirFiles(dir, ["realfile", "readonly", "subdir", "symlink", "broken-symlink"]) + assertDirExist(dir + "/subdir") + assertDirFiles(dir + "/subdir", ["hardlink"]) + assertFileHasContent(dir + "/realfile", "real1") + assertFileHasContent(dir + "/readonly", "readonly") + assertFileHasContent(dir + "/subdir/hardlink", "real1") + assert ( + os.lstat(dir + "/realfile").st_ino == os.lstat(dir + "/subdir/hardlink").st_ino + ) + assertSymlink(dir + "/symlink", "realfile") + assertSymlink(dir + "/broken-symlink", "the-void") + + filepath = docpath + "/a-file" + real_filepath = doc.real_path + "/a-file" + filepath2 = docpath + "/dir/a-file2" + real_filepath2 = doc.real_path + "/dir/a-file2" + + if writable: # We can create files + if os.environ.get("TEST_IN_ROOTED_CI"): + assertRaises(PermissionError, os.open, dir + "/readonly", os.O_RDWR) + os.chmod(dir + "/readonly", 0o700) + fd = os.open(dir + "/readonly", os.O_RDWR) # Works now + os.close(fd) + + setFileContent(filepath, "filedata") + assertFileHasContent(filepath, "filedata") + assertFileHasContent(real_filepath, "filedata") + + fd = os.open(filepath, os.O_RDONLY) + fd2 = os.open(filepath, os.O_RDWR) + assertFdHasContent(fd, "filedata") + assertFdHasContent(fd2, "filedata") + appendFdContent(fd2, "-more") + assertFdHasContent(fd, "filedata-more") + assertFdHasContent(fd2, "filedata-more") + + os.link(filepath, filepath2) + assert os.lstat(filepath).st_ino == os.lstat(filepath2).st_ino + assert os.lstat(filepath).st_ino == os.fstat(fd).st_ino + assertFileHasContent(filepath2, "filedata-more") + assertFileHasContent(real_filepath2, "filedata-more") + + os.unlink(filepath) + assertFileNotExist(filepath) + assertFileNotExist(real_filepath) + assertFdHasContent(fd, "filedata-more") + assertFdHasContent(fd2, "filedata-more") + + replaceFdContent(fd2, "replaced") + assertFileHasContent(filepath2, "replaced") + assertFileHasContent(real_filepath2, "replaced") + assertFileNotExist(filepath) + assertFileNotExist(real_filepath) + assertFdHasContent(fd, "replaced") + assertFdHasContent(fd2, "replaced") + + # Move between dirs + os.rename(filepath2, docpath + "/moved") + assertFileHasContent(docpath + "/moved", "replaced") + + assertRaisesErrno(errno.EXDEV, os.rename, docpath, portal.mountpoint) + + os.unlink(docpath + "/moved") + + os.close(fd) + os.close(fd2) + + os.symlink("realfile", dir + "/symlink2") + os.symlink("the-void", dir + "/broken-symlink2") + assertSymlink(dir + "/symlink2", "realfile") + assertSymlink(dir + "/broken-symlink2", "the-void") + os.unlink(dir + "/symlink2") + os.unlink(dir + "/broken-symlink2") + + else: + # We should be unable to create files + assertRaises( + PermissionError, + os.open, + filepath, + os.O_CREAT | os.O_RDONLY | os.O_TRUNC, + 0o600, + ) + assertRaises( + PermissionError, os.open, filepath, os.O_CREAT | os.O_WRONLY, 0o600 + ) + assertRaises(PermissionError, os.open, filepath, os.O_CREAT | os.O_RDWR, 0o600) + + assertRaises(PermissionError, os.open, dir + "/realfile", os.O_RDWR) + assertRaises(PermissionError, os.open, dir + "/readonly", os.O_RDWR) + assertRaises(PermissionError, os.truncate, dir + "/realfile", 0) + assertRaises(PermissionError, os.link, dir + "/realfile", dir + "/foo") + assertRaises(PermissionError, os.symlink, "foo", dir + "/new-symlink") + assertRaises(PermissionError, os.rename, dir + "/realfile", dir + "/foo") + assertRaises(PermissionError, os.unlink, dir + "/realfile") + assertRaises(PermissionError, os.chmod, dir + "/realfile", 0o700) + assertRaises(PermissionError, os.rmdir, dir + "/subdir") + + os.unlink(real_dir + "/realfile") + os.unlink(real_dir + "/readonly") + os.unlink(real_dir + "/subdir/hardlink") + os.unlink(real_dir + "/symlink") + os.unlink(real_dir + "/broken-symlink") + os.rmdir(real_dir + "/subdir") + os.rmdir(real_dir) + + +def check_doc_perms(doc, app_id): + path = doc.get_doc_path(app_id) + readable = doc.is_readable_by(app_id) + if not readable: + assertRaises(FileNotFoundError, os.lstat, path) + if doc.is_dir: # Non readable dir means we can't even see the toplevel dir + assertRaises(FileNotFoundError, os.mkdir, path) + else: + assertRaises(PermissionError, os.mkdir, path) + return + + assertRaises(PermissionError, os.rmdir, path) + assertRaises(PermissionError, os.rename, path, path + "_renamed") + assertRaises(IsADirectoryError, os.unlink, path) + if doc.is_dir: + check_directory_doc_perms(doc, app_id) + else: + check_regular_doc_perms(doc, app_id) + + +def check_perms(): + check_root_perms(portal.mountpoint) + check_byapp_perms(portal.by_app_path()) + + for doc in portal.get_docs_randomized(): + check_doc_perms(doc, None) + for app_id in portal.get_app_ids_randomized(): + check_doc_perms(doc, app_id) + + +# Ensure that a single lookup by app-id creates that app id (we need this for when mounting the subdir for an app) +def create_app_by_lookup(): + # Should only work for valid app ids + assertRaises(FileNotFoundError, os.lstat, portal.app_path("not-an-app-id")) + + app_id = app_prefix + "Lookup" + info = os.lstat(portal.app_path(app_id)) + check_virtual_stat(info) + portal.ensure_app_id(app_id, volatile=True) + + +def ensure_real_dir(create_hidden_file=True): + count = get_a_count("doc") + dir = TEST_DATA_DIR + "/" + dir_prefix + str(count) + os.makedirs(dir) + if create_hidden_file: + setFileContent(dir + "/cant-see-this-file", "s3krit") + return (dir, count) + + +def ensure_real_dir_file(create_file): + (dir, count) = ensure_real_dir() + path = dir + "/the-file" + if create_file: + setFileContent(path, "data" + str(count)) + return path + + +def export_a_doc(): + path = ensure_real_dir_file(True) + doc = portal.add(path) + logv("exported %s as %s" % (path, doc)) + + lookup = portal.lookup(path) + assert lookup == doc.id + + lookup_on_fuse = portal.lookup(doc.get_doc_path(None) + "/" + doc.filename) + assert lookup_on_fuse == doc.id + + reused_doc = portal.add(path) + assert doc is reused_doc + + not_reused_doc = portal.add(path, False) + assert doc is not not_reused_doc + + # We should not be able to re-export a tmpfile + tmppath = doc.get_doc_path(None) + "/tmpfile" + setFileContent(tmppath, "tempdata") + + # Should not be able to add a tempfile on the fuse mount, or look it up + assertRaises(GLib.Error, portal.add, tmppath) + lookup = portal.lookup(tmppath) + assert lookup == "" + + os.unlink(tmppath) + + +def export_a_named_doc(create_file): + path = ensure_real_dir_file(create_file) + doc = portal.add_named(path) + logv("exported (named) %s as %s" % (path, doc)) + + if create_file: + lookup = portal.lookup(path) + assert lookup == doc.id + + reused_doc = portal.add_named(path) + assert doc is reused_doc + + not_reused_doc = portal.add_named(path, False) + assert doc is not not_reused_doc + + +def export_a_dir_doc(): + (dir, count) = ensure_real_dir(False) + doc = portal.add_dir(dir) + logv("exported (dir) %s as %s" % (dir, doc)) + + lookup = portal.lookup(dir) + assert lookup == doc.id + + lookup_on_fuse = portal.lookup(doc.get_doc_path(None)) + assert lookup_on_fuse == doc.id + + # We should not be able to portal lookup a file in the dir doc + subpath = doc.get_doc_path(None) + "/sub" + setFileContent(subpath, "sub") + doc = portal.lookup(subpath) + assert doc == "" + doc2 = portal.lookup(dir + "/sub") + assert doc2 == "" + + # But we should be able to re-export the file + reexported_doc = portal.add(subpath) + reexported_docdir = reexported_doc.get_doc_path(None) + assertFileHasContent(reexported_docdir + "/sub", "sub") + portal.delete(reexported_doc.id) + + os.unlink(subpath) + + # And also re-export a directory + os.mkdir(subpath) + setFileContent(subpath + "/subfile", "subfile") + reexported_doc = portal.add_dir(subpath) + reexported_docdir = reexported_doc.get_doc_path(None) + assertFileHasContent(reexported_docdir + "/subfile", "subfile") + portal.delete(reexported_doc.id) + + os.unlink(subpath + "/subfile") + os.rmdir(subpath) + + +def add_an_app(num_docs): + if num_docs == 0: + return + count = get_a_count("app") + read_app = app_prefix + "read.App" + str(count) + write_app = app_prefix + "write.App" + str(count) + portal.ensure_app_id(read_app) + portal.ensure_app_id(write_app) + + docs = portal.get_docs() + ids = [] + for i in range(num_docs): + if len(docs) == 0: + continue + indx = random.randint(0, len(docs) - 1) + doc = docs[indx] + del docs[indx] + ids.append(doc.id) + portal.grant_permissions(doc.id, read_app, ["read"]) + doc.apps.append(read_app) + portal.grant_permissions(doc.id, write_app, ["read", "write"]) + doc.apps.append(write_app) + logv("granted acces to %s and %s for %s" % (read_app, write_app, ids)) + + +log("Connecting to portal") +portal = DocPortal() + +log("Running fuse tests...") +create_app_by_lookup() +verify_fs_layout() + +log("Creating some docs") +for i in range(10): + export_a_doc() +verify_fs_layout() + +log("Creating some named docs (existing)") +for i in range(10): + export_a_named_doc(True) +verify_fs_layout() + +log("Creating some named docs (non-existing)") +for i in range(10): + export_a_named_doc(False) +verify_fs_layout() + +log("Creating some dir docs") +for i in range(10): + export_a_dir_doc() +verify_fs_layout() + +log("Creating some apps") +for i in range(10): + add_an_app(6) +verify_fs_layout() + +for i in range(args.iterations): + log("Checking permissions, pass %d" % (i + 1)) + check_perms() + verify_fs_layout() + +log("fuse tests ok") +sys.exit(0) diff --git a/tests/test-document-fuse.sh b/tests/test-document-fuse.sh new file mode 100755 index 0000000..ff0530d --- /dev/null +++ b/tests/test-document-fuse.sh @@ -0,0 +1,102 @@ +#!/bin/bash + + +skip() { + echo "1..0 # SKIP" "$@" + exit 0 +} + +skip_without_fuse () { + fusermount3 --version >/dev/null 2>&1 || skip "no fusermount3" + + capsh --print | grep -q 'Bounding set.*[^a-z]cap_sys_admin' || \ + skip "No cap_sys_admin in bounding set, can't use FUSE" + + [ -w /dev/fuse ] || skip "no write access to /dev/fuse" + [ -e /etc/mtab ] || skip "no /etc/mtab" +} + +skip_without_fuse + +echo "1..2" + +set -e + +if [ -n "${G_TEST_SRCDIR:-}" ]; then + test_srcdir="${G_TEST_SRCDIR}" +else + test_srcdir=$(realpath "$(dirname $0)") +fi + +if [ -n "${G_TEST_BUILDDIR:-}" ]; then + test_builddir="${G_TEST_BUILDDIR}" +else + test_builddir=$(realpath "$(dirname $0)") +fi + +export TEST_DATA_DIR=`mktemp -d /tmp/xdp-XXXXXX` +mkdir -p "${TEST_DATA_DIR}/home" +mkdir -p "${TEST_DATA_DIR}/runtime" +mkdir -p "${TEST_DATA_DIR}/system" +mkdir -p "${TEST_DATA_DIR}/config" + +export HOME=${TEST_DATA_DIR}/home +export XDG_CACHE_HOME=${TEST_DATA_DIR}/home/cache +export XDG_CONFIG_HOME=${TEST_DATA_DIR}/home/config +export XDG_DATA_HOME=${TEST_DATA_DIR}/home/share +export XDG_RUNTIME_DIR=${TEST_DATA_DIR}/runtime + +cleanup () { + fusermount3 -u "$XDG_RUNTIME_DIR/doc" || : + sleep 0.1 + kill "$DBUS_SESSION_BUS_PID" + kill $(jobs -p) &> /dev/null || true + rm -rf "$TEST_DATA_DIR" +} +trap cleanup EXIT + +ITERATIONS=3 +PARALLEL_TESTS=20 +PARALLEL_ITERATIONS=10 + +if [ -n "$TEST_IN_CI" ]; then + PARALLEL_TESTS=10 + PARALLEL_ITERATIONS=5 +fi + +sed "s#@testdir@#${test_builddir}#" "${test_srcdir}/session.conf.in" > session.conf + +dbus-daemon --fork --config-file=session.conf --print-address=3 --print-pid=4 \ + 3> dbus-session-bus-address 4> dbus-session-bus-pid +export DBUS_SESSION_BUS_ADDRESS="$(cat dbus-session-bus-address)" +DBUS_SESSION_BUS_PID="$(cat dbus-session-bus-pid)" + +if ! kill -0 "$DBUS_SESSION_BUS_PID"; then + assert_not_reached "Failed to start dbus-daemon" +fi + +# Run portal manually so that we get any segfault our assert output +# Add -v here to get debug output from fuse +# Only do this when running uninstalled; when running as an installed-test, +# we rely on D-Bus activation. +if [ -n "${XDP_UNINSTALLED:-}" ]; then + ./xdg-document-portal -r & +fi + +# First run a basic single-thread test +echo Testing single-threaded +"${test_srcdir}/test-document-fuse.py" --iterations ${ITERATIONS} -v +echo "ok single-threaded" + +# Then a bunch of copies in parallel to stress-test +echo Testing in parallel +PIDS=() +for i in $(seq ${PARALLEL_TESTS}); do + "${test_srcdir}/test-document-fuse.py" --iterations ${PARALLEL_ITERATIONS} --prefix "$i" & + PID="$!" + PIDS+=( "$PID" ) +done + +echo waiting for pids "${PIDS[@]}" +wait "${PIDS[@]}" +echo "ok load-test" diff --git a/tests/test-permission-store.c b/tests/test-permission-store.c new file mode 100644 index 0000000..87d4099 --- /dev/null +++ b/tests/test-permission-store.c @@ -0,0 +1,692 @@ +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "xdp-utils.h" +#include "document-portal/permission-store-dbus.h" +#include "src/glib-backports.h" +#include "utils.h" + +char outdir[] = "/tmp/xdp-test-XXXXXX"; + +GTestDBus *dbus; +GDBusConnection *session_bus; +XdgPermissionStore *permissions; + +static void +test_version (void) +{ + g_assert_cmpint (xdg_permission_store_get_version (permissions), ==, 2); +} + +static int change_count; + +static void +changed_cb (XdgPermissionStore *store, + const char *table, + const char *id, + gboolean deleted, + GVariant *data, + GVariant *perms, + gpointer user_data) +{ + g_autofree char **strv = NULL; + gboolean res; + + change_count++; + + g_assert_cmpstr (table, ==, "TEST"); + g_assert_cmpstr (id, ==, "test-resource"); + g_assert_false (deleted); + g_assert_true (g_variant_is_of_type (perms, G_VARIANT_TYPE ("a{sas}"))); + res = g_variant_lookup (perms, "one.two.three", "^a&s", &strv); + g_assert_true (res); + g_assert_cmpint (g_strv_length (strv), ==, 2); + g_assert (g_strv_contains ((const char *const *)strv, "one")); + g_assert (g_strv_contains ((const char *const *)strv, "two")); +} + +static void +changed_cb2 (XdgPermissionStore *store, + const char *table, + const char *id, + gboolean deleted, + GVariant *data, + GVariant *perms, + gpointer user_data) +{ + change_count++; + + g_assert_cmpstr (table, ==, "TEST"); + g_assert_cmpstr (id, ==, "test-resource"); + g_assert_true (deleted); +} + +static gboolean +timeout_cb (gpointer data) +{ + gboolean *timeout_reached = data; + + *timeout_reached = TRUE; + return G_SOURCE_CONTINUE; +} + +static void +test_change (void) +{ + gulong changed_handler; + gboolean res; + g_autoptr(GError) error = NULL; + const char * perms[] = { "one", "two", NULL }; + gboolean timeout_reached = FALSE; + guint timeout_id; + + changed_handler = g_signal_connect (permissions, "changed", G_CALLBACK (changed_cb), NULL); + + change_count = 0; + + res = xdg_permission_store_call_set_permission_sync (permissions, + "TEST", TRUE, + "test-resource", + "one.two.three", + perms, + NULL, + &error); + g_assert_no_error (error); + g_assert_true (res); + + timeout_id = g_timeout_add (10000, timeout_cb, &timeout_reached); + while (!timeout_reached && change_count == 0) + g_main_context_iteration (NULL, TRUE); + g_source_remove (timeout_id); + + g_assert_cmpint (change_count, ==, 1); + + g_signal_handler_disconnect (permissions, changed_handler); + + changed_handler = g_signal_connect (permissions, "changed", G_CALLBACK (changed_cb2), NULL); + + change_count = 0; + + res = xdg_permission_store_call_delete_sync (permissions, + "TEST", + "test-resource", + NULL, + &error); + g_assert_no_error (error); + g_assert_true (res); + + timeout_id = g_timeout_add (10000, timeout_cb, &timeout_reached); + while (!timeout_reached && change_count == 0) + g_main_context_iteration (NULL, TRUE); + g_source_remove (timeout_id); + + g_assert_cmpint (change_count, ==, 1); + + g_signal_handler_disconnect (permissions, changed_handler); +} + +static void +test_lookup (void) +{ + gboolean res; + g_autoptr(GError) error = NULL; + const char * perms[] = { "one", "two", NULL }; + g_autoptr(GVariant) p = NULL; + g_autoptr(GVariant) d = NULL; + g_autofree char **strv = NULL; + GVariantBuilder pb; + + res = xdg_permission_store_call_lookup_sync (permissions, + "TEST", + "test-resource", + &p, + &d, + NULL, + &error); + g_assert_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_FOUND); + g_assert_false (res); + g_clear_error (&error); + + g_variant_builder_init (&pb, G_VARIANT_TYPE ("a{sas}")); + g_variant_builder_add (&pb, "{s@as}", "one.two.three", g_variant_new_strv (perms, -1)); + res = xdg_permission_store_call_set_sync (permissions, + "TEST", TRUE, + "test-resource", + g_variant_builder_end (&pb), + g_variant_new_variant (g_variant_new_boolean (TRUE)), + NULL, + &error); + g_assert_no_error (error); + g_assert_true (res); + + res = xdg_permission_store_call_lookup_sync (permissions, + "TEST", + "test-resource", + &p, + &d, + NULL, + &error); + g_assert_no_error (error); + g_assert_true (res); + + g_assert_true (g_variant_is_of_type (p, G_VARIANT_TYPE ("a{sas}"))); + res = g_variant_lookup (p, "one.two.three", "^a&s", &strv); + g_assert_true (res); + g_assert_cmpint (g_strv_length (strv), ==, 2); + g_assert (g_strv_contains ((const char *const *)strv, "one")); + g_assert (g_strv_contains ((const char *const *)strv, "two")); + g_assert_true (g_variant_is_of_type (d, G_VARIANT_TYPE_VARIANT)); + g_assert_true (g_variant_is_of_type (g_variant_get_variant (d), G_VARIANT_TYPE_BOOLEAN)); + g_assert_true (g_variant_get_boolean (g_variant_get_variant (d))); + + res = xdg_permission_store_call_delete_sync (permissions, + "TEST", + "test-resource", + NULL, + &error); + g_assert_no_error (error); +} + +static void +test_set_value (void) +{ + gboolean res; + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) p = NULL; + g_autoptr(GVariant) d = NULL; + + res = xdg_permission_store_call_lookup_sync (permissions, + "TEST", + "test-resource", + &p, + &d, + NULL, + &error); + g_assert_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_FOUND); + g_assert_false (res); + g_clear_error (&error); + + res = xdg_permission_store_call_set_value_sync (permissions, + "TEST", TRUE, + "test-resource", + g_variant_new_variant (g_variant_new_boolean (TRUE)), + NULL, + &error); + g_assert_no_error (error); + g_assert_true (res); + + res = xdg_permission_store_call_lookup_sync (permissions, + "TEST", + "test-resource", + &p, + &d, + NULL, + &error); + g_assert_no_error (error); + g_assert_true (res); + + g_assert_true (g_variant_is_of_type (p, G_VARIANT_TYPE ("a{sas}"))); + g_assert_cmpint (g_variant_n_children (p), ==, 0); + g_assert_true (res); + g_assert_true (g_variant_is_of_type (d, G_VARIANT_TYPE_VARIANT)); + g_assert_true (g_variant_is_of_type (g_variant_get_variant (d), G_VARIANT_TYPE_BOOLEAN)); + g_assert_true (g_variant_get_boolean (g_variant_get_variant (d))); + + res = xdg_permission_store_call_delete_sync (permissions, + "TEST", + "test-resource", + NULL, + &error); + g_assert_no_error (error); +} + +static void +test_create1 (void) +{ + gboolean res; + g_autoptr(GError) error = NULL; + const char * perms[] = { "one", "two", NULL }; + + res = xdg_permission_store_call_set_permission_sync (permissions, + "DOESNOTEXIST", FALSE, + "test-resource", + "one.two.three", + perms, + NULL, + &error); + g_assert_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_FOUND); + g_assert_false (res); +} + +static void +test_create2 (void) +{ + gboolean res; + g_autoptr(GError) error = NULL; + const char * perms[] = { "logout", "suspend", NULL }; + + res = xdg_permission_store_call_set_permission_sync (permissions, + "inhibit", + TRUE, + "inhibit", + "", + perms, + NULL, + &error); + g_assert_no_error (error); + g_assert_true (res); +} + +static void +test_delete1 (void) +{ + gboolean res; + g_autoptr(GError) error = NULL; + + res = xdg_permission_store_call_delete_sync (permissions, + "inhibit", + "no-such-entry", + NULL, + &error); + g_assert_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_FOUND); + g_assert_false (res); +} + +static void +test_delete2 (void) +{ + gboolean res; + g_autoptr(GError) error = NULL; + const char * perms[] = { "logout", "suspend", NULL }; + g_autoptr(GVariant) out_perms = NULL; + g_autoptr(GVariant) out_data = NULL; + + res = xdg_permission_store_call_set_permission_sync (permissions, + "inhibit", + TRUE, + "inhibit", + "", + perms, + NULL, + &error); + g_assert_no_error (error); + g_assert_true (res); + + res = xdg_permission_store_call_delete_sync (permissions, + "inhibit", + "inhibit", + NULL, + &error); + g_assert_no_error (error); + g_assert_true (res); + + res = xdg_permission_store_call_lookup_sync (permissions, + "inhibit", + "inhibit", + &out_perms, + &out_data, + NULL, + &error); + g_assert_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_FOUND); + g_assert_false (res); +} + +static int got_result; + +static void +set_cb (GObject *object, + GAsyncResult *result, + gpointer data) +{ + g_autoptr(GError) error = NULL; + + xdg_permission_store_call_set_permission_finish (permissions, result, &error); + g_assert_no_error (error); + + got_result++; + g_main_context_wakeup (NULL); +} + +static void +delete_cb (GObject *object, + GAsyncResult *result, + gpointer data) +{ + g_autoptr(GError) error = NULL; + + xdg_permission_store_call_delete_finish (permissions, result, &error); + g_assert_no_error (error); + + got_result++; + g_main_context_wakeup (NULL); +} + +static void +test_delete3 (void) +{ + const char * perms[] = { "logout", "suspend", NULL }; + g_autoptr(GVariant) out_perms = NULL; + g_autoptr(GVariant) out_data = NULL; + gboolean res; + g_autoptr(GError) error = NULL; + + got_result = 0; + xdg_permission_store_call_set_permission (permissions, "inhibit", TRUE, "inhibit", "", perms, NULL, set_cb, NULL); + xdg_permission_store_call_delete (permissions, "inhibit", "inhibit", NULL, delete_cb, NULL); + + while (got_result < 2) + g_main_context_iteration (NULL, TRUE); + + res = xdg_permission_store_call_lookup_sync (permissions, + "inhibit", + "inhibit", + &out_perms, + &out_data, + NULL, + &error); + g_assert_false (res); + g_assert_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_FOUND); +} + +static void +delete_permission_cb (GObject *object, + GAsyncResult *result, + gpointer data) +{ + g_autoptr(GError) error = NULL; + + xdg_permission_store_call_delete_permission_finish (permissions, result, &error); + g_assert_no_error (error); + + got_result++; + g_main_context_wakeup (NULL); +} + +static void +test_delete4 (void) +{ + const char * perms[] = { "logout", "suspend", NULL }; + g_autoptr(GVariant) out_perms = NULL; + g_autoptr(GVariant) out_data = NULL; + g_autoptr(GVariant) expected = NULL; + gboolean res; + g_autoptr(GError) error = NULL; + + got_result = 0; + xdg_permission_store_call_set_permission (permissions, "inhibit", TRUE, "inhibit", "a", perms, NULL, set_cb, NULL); + xdg_permission_store_call_set_permission (permissions, "inhibit", TRUE, "inhibit", "b", perms, NULL, set_cb, NULL); + xdg_permission_store_call_delete_permission (permissions, "inhibit", "inhibit", "a", NULL, delete_permission_cb, NULL); + + while (got_result < 3) + g_main_context_iteration (NULL, TRUE); + + expected = g_variant_parse (G_VARIANT_TYPE ("a{sas}"), "{\"b\": [\"logout\",\"suspend\"]}", NULL, NULL, NULL); + + res = xdg_permission_store_call_lookup_sync (permissions, + "inhibit", + "inhibit", + &out_perms, + &out_data, + NULL, + &error); + g_assert_true (res); + g_assert_no_error (error); + + g_assert_true (g_variant_equal (expected, out_perms)); +} + +static void +test_delete5 (void) +{ + const char * perms[] = { "yes", NULL }; + g_autoptr(GVariant) out_perms = NULL; + g_autoptr(GVariant) out_data = NULL; + g_autoptr(GVariant) expected = NULL; + gboolean res; + g_autoptr(GError) error = NULL; + + got_result = 0; + xdg_permission_store_call_set_permission (permissions, "notifications", TRUE, "notification", "a", perms, NULL, set_cb, NULL); + xdg_permission_store_call_delete_permission (permissions, "notifications", "notification", "a", NULL, delete_permission_cb, NULL); + + while (got_result < 2) + g_main_context_iteration (NULL, TRUE); + + /* it did not crash during delete permission */ + g_assert_cmpint (got_result, ==, 2); + + res = xdg_permission_store_call_lookup_sync (permissions, + "notifications", + "notification", + &out_perms, + &out_data, + NULL, + &error); + + + expected = g_variant_new_array (G_VARIANT_TYPE ("{sas}"), NULL, 0); + + g_assert_true (res); + g_assert_no_error (error); + + /* an empty entry is left instead */ + g_assert_true (g_variant_equal (expected, out_perms)); +} + +static void +test_get_permission1 (void) +{ + gboolean res; + g_autoptr(GError) error = NULL; + g_autofree char **out_perms = NULL; + + res = xdg_permission_store_call_get_permission_sync (permissions, + "no-such-table", + "no-such-entry", + "no-such-app", + &out_perms, + NULL, + &error); + g_assert_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_FOUND); + g_assert_false (res); +} + +static void +test_get_permission2 (void) +{ + gboolean res; + const char * in_perms[] = { "yes", NULL }; + g_autofree char **out_perms = NULL; + g_autoptr(GError) error = NULL; + + res = xdg_permission_store_call_set_permission_sync (permissions, + "notifications", + TRUE, + "notification", + "a", + in_perms, + NULL, + &error); + g_assert_no_error (error); + g_assert_true (res); + + res = xdg_permission_store_call_get_permission_sync (permissions, + "notifications", + "notification", + "a", + &out_perms, + NULL, + &error); + g_assert_true (res); + g_assert_no_error (error); + g_assert (g_strv_length (out_perms) == 1); + g_assert (g_strv_contains ((const char *const *)out_perms, "yes")); +} + +static void +test_get_permission3 (void) +{ + gboolean res; + g_autofree char **out_perms = NULL; + g_autoptr(GError) error = NULL; + + res = xdg_permission_store_call_get_permission_sync (permissions, + "notifications", + "notification", + "no-such-app", + &out_perms, + NULL, + &error); + g_assert_true (res); + g_assert_no_error (error); + g_assert (g_strv_length (out_perms) == 0); +} + +static void +global_setup (void) +{ + GError *error = NULL; + g_autofree gchar *services = NULL; + GQuark portal_errors G_GNUC_UNUSED; + + /* make sure errors are registered */ + portal_errors = XDG_DESKTOP_PORTAL_ERROR; + + g_mkdtemp (outdir); + g_debug ("outdir: %s\n", outdir); + + g_setenv ("XDG_RUNTIME_DIR", outdir, TRUE); + g_setenv ("XDG_DATA_HOME", outdir, TRUE); + + /* Re-defining dbus-monitor with a custom script */ + setup_dbus_daemon_wrapper (outdir); + + dbus = g_test_dbus_new (G_TEST_DBUS_NONE); + services = g_test_build_filename (G_TEST_BUILT, "services", NULL); + g_test_dbus_add_service_dir (dbus, services); + g_test_dbus_up (dbus); + + /* g_test_dbus_up unsets this, so re-set */ + g_setenv ("XDG_RUNTIME_DIR", outdir, TRUE); + + session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); + g_assert_no_error (error); + + permissions = xdg_permission_store_proxy_new_sync (session_bus, 0, + "org.freedesktop.impl.portal.PermissionStore", + "/org/freedesktop/impl/portal/PermissionStore", + NULL, &error); + g_assert_no_error (error); + g_assert (permissions != NULL); +} + +static gboolean +rm_rf_dir (GFile *dir, + GError **error) +{ + GFileEnumerator *enumerator = NULL; + g_autoptr(GFileInfo) child_info = NULL; + GError *temp_error = NULL; + + enumerator = g_file_enumerate_children (dir, "standard::type,standard::name", + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + NULL, error); + if (!enumerator) + return FALSE; + + while ((child_info = g_file_enumerator_next_file (enumerator, NULL, &temp_error))) + { + const char *name = g_file_info_get_name (child_info); + g_autoptr(GFile) child = g_file_get_child (dir, name); + + if (g_file_info_get_file_type (child_info) == G_FILE_TYPE_DIRECTORY) + { + if (!rm_rf_dir (child, error)) + return FALSE; + } + else + { + if (!g_file_delete (child, NULL, error)) + return FALSE; + } + + g_clear_object (&child_info); + } + + if (temp_error != NULL) + { + g_propagate_error (error, temp_error); + return FALSE; + } + + if (!g_file_delete (dir, NULL, error)) + return FALSE; + + return TRUE; +} + +static void +global_teardown (void) +{ + GError *error = NULL; + g_autoptr(GFile) outdir_file = g_file_new_for_path (outdir); + int res; + + g_object_unref (permissions); + + g_dbus_connection_close_sync (session_bus, NULL, &error); + g_assert_no_error (error); + + g_object_unref (session_bus); + + g_test_dbus_down (dbus); + + g_object_unref (dbus); + + res = rm_rf_dir (outdir_file, &error); + g_assert_no_error (error); + g_assert_true (res); +} + +int +main (int argc, char **argv) +{ + int res; + + g_log_writer_default_set_use_stderr (TRUE); + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/permissions/version", test_version); + g_test_add_func ("/permissions/change", test_change); + g_test_add_func ("/permissions/lookup", test_lookup); + g_test_add_func ("/permissions/delete1", test_delete1); + g_test_add_func ("/permissions/delete2", test_delete2); + g_test_add_func ("/permissions/delete3", test_delete3); + g_test_add_func ("/permissions/delete4", test_delete4); + g_test_add_func ("/permissions/delete5", test_delete5); + g_test_add_func ("/permissions/create1", test_create1); + g_test_add_func ("/permissions/create2", test_create2); + g_test_add_func ("/permissions/set-value", test_set_value); + g_test_add_func ("/permissions/get-permission1", test_get_permission1); + g_test_add_func ("/permissions/get-permission2", test_get_permission2); + g_test_add_func ("/permissions/get-permission3", test_get_permission3); + + global_setup (); + + res = g_test_run (); + + global_teardown (); + + return res; +} diff --git a/tests/test-portals.c b/tests/test-portals.c new file mode 100644 index 0000000..bae93a5 --- /dev/null +++ b/tests/test-portals.c @@ -0,0 +1,599 @@ +#include +#include +#include + +#include + +#include "src/glib-backports.h" +#include "xdp-dbus.h" +#include "xdp-utils.h" +#include "xdp-impl-dbus.h" + +#ifdef HAVE_LIBPORTAL +#include "account.h" +#include "background.h" +#include "camera.h" +#include "email.h" +#include "filechooser.h" +#include "inhibit.h" +#include "location.h" +#include "notification.h" +#include "openuri.h" +#include "print.h" +#include "screenshot.h" +#include "trash.h" +#include "wallpaper.h" +#endif + +#include "utils.h" + +/* required while we support meson + autotools. Autotools builds everything in + the root dir ('.'), meson builds in each subdir nested and overrides these for + g_test_build_filename */ +#ifndef XDG_DP_BUILDDIR +#define XDG_DP_BUILDDIR "." +#endif +#ifndef XDG_PS_BUILDDIR +#define XDG_PS_BUILDDIR "." +#endif + +#define PORTAL_BUS_NAME "org.freedesktop.portal.Desktop" +#define PORTAL_OBJECT_PATH "/org/freedesktop/portal/desktop" +#define BACKEND_BUS_NAME "org.freedesktop.impl.portal.Test" +#define BACKEND_OBJECT_PATH "/org/freedesktop/portal/desktop" + +#include "document-portal/permission-store-dbus.h" + +char outdir[] = "/tmp/xdp-test-XXXXXX"; + +static GTestDBus *dbus; +static GDBusConnection *session_bus; +static GList *test_procs = NULL; +XdpDbusImplPermissionStore *permission_store; +XdpDbusImplLockdown *lockdown; + +int +xdup (int oldfd) +{ + int newfd = dup (oldfd); + + if (newfd < 0) + g_error ("Unable to duplicate fd %d: %s", oldfd, g_strerror (errno)); + + return newfd; +} + +static void +name_appeared_cb (GDBusConnection *bus, + const char *name, + const char *name_owner, + gpointer data) +{ + gboolean *b = (gboolean *)data; + + g_debug ("Name %s now owned by %s\n", name, name_owner); + + *b = TRUE; + + g_main_context_wakeup (NULL); +} + +static void +name_disappeared_cb (GDBusConnection *bus, + const char *name, + gpointer data) +{ + g_debug ("Name %s disappeared\n", name); +} + +static gboolean +timeout_cb (gpointer data) +{ + const char *msg = data; + + g_error ("%s", msg); + + return G_SOURCE_REMOVE; +} + +static void +update_data_dirs (void) +{ + const char *data_dirs; + gssize len = 0; + GString *str; + char *new_val; + + data_dirs = g_getenv ("XDG_DATA_DIRS"); + if (data_dirs != NULL && + strstr (data_dirs, "/usr/share") != NULL) + { + return; + } + + if (data_dirs != NULL) + { + len = strlen (data_dirs); + if (data_dirs[len] == ':') + len--; + } + + str = g_string_new_len (data_dirs, len); + if (str->len > 0) + g_string_append_c (str, ':'); + g_string_append (str, "/usr/local/share/:/usr/share/"); + new_val = g_string_free (str, FALSE); + + g_debug ("Setting XDG_DATA_DIRS to %s", new_val); + g_setenv ("XDG_DATA_DIRS", new_val, TRUE); + /* new_val is leaked */ +} + +static void +global_setup (void) +{ + GError *error = NULL; + g_autofree gchar *backends_executable = NULL; + g_autofree gchar *services = NULL; + g_autofree gchar *portal_dir = NULL; + g_autofree gchar *argv0 = NULL; + g_autoptr(GSubprocessLauncher) launcher = NULL; + g_autoptr(GSubprocess) subprocess = NULL; + guint name_timeout; + const char *argv[4]; + GQuark portal_errors G_GNUC_UNUSED; + static gboolean name_appeared; + guint watch; + guint timeout_mult = 1; + + update_data_dirs (); + + g_mkdtemp (outdir); + g_debug ("outdir: %s\n", outdir); + + g_setenv ("XDG_CURRENT_DESKTOP", "test", TRUE); + g_setenv ("XDG_RUNTIME_DIR", outdir, TRUE); + g_setenv ("XDG_DATA_HOME", outdir, TRUE); + + /* Re-defining dbus-daemon with a custom script */ + setup_dbus_daemon_wrapper (outdir); + + dbus = g_test_dbus_new (G_TEST_DBUS_NONE); + services = g_test_build_filename (G_TEST_BUILT, "services", NULL); + g_test_dbus_add_service_dir (dbus, services); + g_test_dbus_up (dbus); + + if (g_getenv ("TEST_IN_CI")) + timeout_mult = 10; + + /* g_test_dbus_up unsets this, so re-set */ + g_setenv ("XDG_RUNTIME_DIR", outdir, TRUE); + + session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); + g_assert_no_error (error); + + /* start portal backends */ + name_appeared = FALSE; + watch = g_bus_watch_name_on_connection (session_bus, + BACKEND_BUS_NAME, + 0, + name_appeared_cb, + name_disappeared_cb, + &name_appeared, + NULL); + + launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_NONE); + g_subprocess_launcher_setenv (launcher, "G_DEBUG", "fatal-criticals", TRUE); + g_subprocess_launcher_setenv (launcher, "DBUS_SESSION_BUS_ADDRESS", g_test_dbus_get_bus_address (dbus), TRUE); + g_subprocess_launcher_setenv (launcher, "XDG_DATA_HOME", outdir, TRUE); + g_subprocess_launcher_setenv (launcher, "PATH", g_getenv ("PATH"), TRUE); + g_subprocess_launcher_take_stdout_fd (launcher, xdup (STDERR_FILENO)); + + backends_executable = g_test_build_filename (G_TEST_BUILT, "test-backends", NULL); + argv[0] = backends_executable; + argv[1] = "--backend-name=" BACKEND_BUS_NAME; + argv[2] = g_test_verbose () ? "--verbose" : NULL; + argv[3] = NULL; + + g_debug ("launching test-backend\n"); + + subprocess = g_subprocess_launcher_spawnv (launcher, argv, &error); + g_assert_no_error (error); + g_test_message ("Launched %s with pid %s\n", argv[0], + g_subprocess_get_identifier (subprocess)); + test_procs = g_list_append (test_procs, g_steal_pointer (&subprocess)); + + name_timeout = g_timeout_add (1000 * timeout_mult, timeout_cb, "Failed to launch test-backends"); + + while (!name_appeared) + g_main_context_iteration (NULL, TRUE); + + g_source_remove (name_timeout); + g_bus_unwatch_name (watch); + + /* start permission store */ + name_appeared = FALSE; + watch = g_bus_watch_name_on_connection (session_bus, + "org.freedesktop.impl.portal.PermissionStore", + 0, + name_appeared_cb, + name_disappeared_cb, + &name_appeared, + NULL); + + g_clear_object (&launcher); + launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_NONE); + g_subprocess_launcher_setenv (launcher, "G_DEBUG", "fatal-criticals", TRUE); + g_subprocess_launcher_setenv (launcher, "DBUS_SESSION_BUS_ADDRESS", g_test_dbus_get_bus_address (dbus), TRUE); + g_subprocess_launcher_setenv (launcher, "XDG_DATA_HOME", outdir, TRUE); + g_subprocess_launcher_setenv (launcher, "PATH", g_getenv ("PATH"), TRUE); + g_subprocess_launcher_take_stdout_fd (launcher, xdup (STDERR_FILENO)); + + if (g_getenv ("XDP_UNINSTALLED") != NULL) + argv0 = g_test_build_filename (G_TEST_BUILT, "..", XDG_PS_BUILDDIR, "xdg-permission-store", NULL); + else + argv0 = g_strdup (LIBEXECDIR "/xdg-permission-store"); + + argv[0] = argv0; + argv[1] = "--replace"; + argv[2] = g_test_verbose () ? "--verbose" : NULL; + argv[3] = NULL; + + g_debug ("launching %s\n", argv0); + + subprocess = g_subprocess_launcher_spawnv (launcher, argv, &error); + g_assert_no_error (error); + g_test_message ("Launched %s with pid %s\n", argv[0], + g_subprocess_get_identifier (subprocess)); + test_procs = g_list_append (test_procs, g_steal_pointer (&subprocess)); + + name_timeout = g_timeout_add (1000 * timeout_mult, timeout_cb, "Failed to launch xdg-permission-store"); + + while (!name_appeared) + g_main_context_iteration (NULL, TRUE); + + g_source_remove (name_timeout); + g_bus_unwatch_name (watch); + + /* start portals */ + name_appeared = FALSE; + watch = g_bus_watch_name_on_connection (session_bus, + PORTAL_BUS_NAME, + 0, + name_appeared_cb, + name_disappeared_cb, + &name_appeared, + NULL); + + portal_dir = g_test_build_filename (G_TEST_BUILT, "portals", "test", NULL); + + g_clear_object (&launcher); + launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_NONE); + g_subprocess_launcher_setenv (launcher, "G_DEBUG", "fatal-criticals", TRUE); + g_subprocess_launcher_setenv (launcher, "DBUS_SESSION_BUS_ADDRESS", g_test_dbus_get_bus_address (dbus), TRUE); + g_subprocess_launcher_setenv (launcher, "XDG_DESKTOP_PORTAL_DIR", portal_dir, TRUE); + g_subprocess_launcher_setenv (launcher, "XDG_DATA_HOME", outdir, TRUE); + g_subprocess_launcher_setenv (launcher, "PATH", g_getenv ("PATH"), TRUE); + g_subprocess_launcher_take_stdout_fd (launcher, xdup (STDERR_FILENO)); + + if (g_getenv ("XDP_UNINSTALLED") != NULL) + argv0 = g_test_build_filename (G_TEST_BUILT, "..", XDG_DP_BUILDDIR, "xdg-desktop-portal", NULL); + else + argv0 = g_strdup (LIBEXECDIR "/xdg-desktop-portal"); + + argv[0] = argv0; + argv[1] = g_test_verbose () ? "--verbose" : NULL; + argv[2] = NULL; + + g_debug ("launching %s\n", argv0); + + subprocess = g_subprocess_launcher_spawnv (launcher, argv, &error); + g_assert_no_error (error); + g_test_message ("Launched %s with pid %s\n", argv[0], + g_subprocess_get_identifier (subprocess)); + test_procs = g_list_append (test_procs, g_steal_pointer (&subprocess)); + g_clear_pointer (&argv0, g_free); + + name_timeout = g_timeout_add (1000 * timeout_mult, timeout_cb, "Failed to launch xdg-desktop-portal"); + + while (!name_appeared) + g_main_context_iteration (NULL, TRUE); + + g_source_remove (name_timeout); + g_bus_unwatch_name (watch); + + permission_store = xdp_dbus_impl_permission_store_proxy_new_sync (session_bus, + 0, + "org.freedesktop.impl.portal.PermissionStore", + "/org/freedesktop/impl/portal/PermissionStore", + NULL, + &error); + g_assert_no_error (error); + + lockdown = xdp_dbus_impl_lockdown_proxy_new_sync (session_bus, + 0, + BACKEND_BUS_NAME, + BACKEND_OBJECT_PATH, + NULL, + &error); + g_assert_no_error (error); + + /* make sure errors are registered */ + portal_errors = XDG_DESKTOP_PORTAL_ERROR; +} + +static void +wait_for_test_procs (void) +{ + GList *l; + + for (l = test_procs; l; l = l->next) + { + GSubprocess *subprocess = G_SUBPROCESS (l->data); + GError *error = NULL; + g_autofree char *identifier = NULL; + + identifier = g_strdup (g_subprocess_get_identifier (subprocess)); + + g_debug ("Terminating and waiting for process %s", identifier); + g_subprocess_send_signal (subprocess, SIGTERM); + + /* This may lead the test to hang, we assume that the test suite or CI + * can handle the case at upper level, without having us async function + * and timeouts */ + g_subprocess_wait (subprocess, NULL, &error); + g_assert_no_error (error); + g_assert_null (g_subprocess_get_identifier (subprocess)); + + if (!g_subprocess_get_if_exited (subprocess)) + { + g_assert_true (g_subprocess_get_if_signaled (subprocess)); + g_assert_cmpint (g_subprocess_get_term_sig (subprocess), ==, SIGTERM); + } + else if (!g_subprocess_get_successful (subprocess)) + { + g_error ("Subprocess %s, exited with exit status %d", identifier, + g_subprocess_get_exit_status (subprocess)); + } + } +} + +static void +global_teardown (void) +{ + GError *error = NULL; + + g_dbus_connection_flush_sync (session_bus, NULL, &error); + g_assert_no_error (error); + + g_dbus_connection_close_sync (session_bus, NULL, &error); + g_assert_no_error (error); + + wait_for_test_procs (); + g_list_free_full (g_steal_pointer (&test_procs), g_object_unref); + + g_object_unref (lockdown); + g_object_unref (permission_store); + + g_object_unref (session_bus); + + g_test_dbus_down (dbus); + + g_object_unref (dbus); +} + +#ifdef HAVE_GEOCLUE +#define check_geoclue(name) +#else +#define check_geoclue(name) \ + if (strcmp (name , "location") == 0) \ + { \ + g_test_skip ("Skipping tests that require geoclue"); \ + return; \ + } +#endif + +/* Just check that the portal is there, and has the + * expected version. This will fail if the backend + * is not found. + */ +#define DEFINE_TEST_EXISTS(pp,PP,version) \ +static void \ +test_##pp##_exists (void) \ +{ \ + g_autoptr(GDBusProxy) proxy = NULL; \ + g_autoptr(GError) error = NULL; \ + g_autofree char *owner = NULL; \ + \ + check_geoclue ( #pp ) \ + \ + proxy = G_DBUS_PROXY (xdp_dbus_##pp##_proxy_new_sync (session_bus, \ + 0, \ + PORTAL_BUS_NAME, \ + PORTAL_OBJECT_PATH, \ + NULL, \ + &error)); \ + g_assert_no_error (error); \ + \ + owner = g_dbus_proxy_get_name_owner (proxy); \ + g_assert_nonnull (owner); \ + \ + g_assert_cmpuint (xdp_dbus_##pp##_get_version (XDP_DBUS_##PP (proxy)), ==, version); \ +} + +DEFINE_TEST_EXISTS(account, ACCOUNT, 1) +DEFINE_TEST_EXISTS(background, BACKGROUND, 2) +DEFINE_TEST_EXISTS(camera, CAMERA, 1) +DEFINE_TEST_EXISTS(email, EMAIL, 3) +DEFINE_TEST_EXISTS(file_chooser, FILE_CHOOSER, 3) +DEFINE_TEST_EXISTS(game_mode, GAME_MODE, 4) +DEFINE_TEST_EXISTS(inhibit, INHIBIT, 3) +DEFINE_TEST_EXISTS(location, LOCATION, 1) +DEFINE_TEST_EXISTS(network_monitor, NETWORK_MONITOR, 3) +DEFINE_TEST_EXISTS(notification, NOTIFICATION, 1) +DEFINE_TEST_EXISTS(open_uri, OPEN_URI, 3) +DEFINE_TEST_EXISTS(print, PRINT, 1) +DEFINE_TEST_EXISTS(proxy_resolver, PROXY_RESOLVER, 1) +DEFINE_TEST_EXISTS(screenshot, SCREENSHOT, 2) +DEFINE_TEST_EXISTS(settings, SETTINGS, 1) +DEFINE_TEST_EXISTS(trash, TRASH, 1) +DEFINE_TEST_EXISTS(wallpaper, WALLPAPER, 1) +DEFINE_TEST_EXISTS(realtime, REALTIME, 1) + +int +main (int argc, char **argv) +{ + int res; + + g_log_writer_default_set_use_stderr (TRUE); + + setlocale (LC_ALL, NULL); + + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/portal/account/exists", test_account_exists); + g_test_add_func ("/portal/background/exists", test_background_exists); + g_test_add_func ("/portal/camera/exists", test_camera_exists); + g_test_add_func ("/portal/email/exists", test_email_exists); + g_test_add_func ("/portal/filechooser/exists", test_file_chooser_exists); + g_test_add_func ("/portal/gamemode/exists", test_game_mode_exists); + g_test_add_func ("/portal/inhibit/exists", test_inhibit_exists); + g_test_add_func ("/portal/location/exists", test_location_exists); + g_test_add_func ("/portal/networkmonitor/exists", test_network_monitor_exists); + g_test_add_func ("/portal/notification/exists", test_notification_exists); + g_test_add_func ("/portal/openuri/exists", test_open_uri_exists); + g_test_add_func ("/portal/print/exists", test_print_exists); + g_test_add_func ("/portal/proxyresolver/exists", test_proxy_resolver_exists); + g_test_add_func ("/portal/screenshot/exists", test_screenshot_exists); + g_test_add_func ("/portal/settings/exists", test_settings_exists); + g_test_add_func ("/portal/trash/exists", test_trash_exists); + g_test_add_func ("/portal/wallpaper/exists", test_wallpaper_exists); + g_test_add_func ("/portal/realtime/exists", test_realtime_exists); + +#ifdef HAVE_LIBPORTAL + g_test_add_func ("/portal/account/basic", test_account_basic); + g_test_add_func ("/portal/account/delay", test_account_delay); + g_test_add_func ("/portal/account/cancel", test_account_cancel); + g_test_add_func ("/portal/account/close", test_account_close); + g_test_add_func ("/portal/account/reason", test_account_reason); + g_test_add_func ("/portal/account/parallel", test_account_parallel); + + g_test_add_func ("/portal/email/basic", test_email_basic); + g_test_add_func ("/portal/email/delay", test_email_delay); + g_test_add_func ("/portal/email/cancel", test_email_cancel); + g_test_add_func ("/portal/email/close", test_email_close); + g_test_add_func ("/portal/email/address", test_email_address); + g_test_add_func ("/portal/email/subject", test_email_subject); + g_test_add_func ("/portal/email/parallel", test_email_parallel); + + g_test_add_func ("/portal/screenshot/basic", test_screenshot_basic); + g_test_add_func ("/portal/screenshot/delay", test_screenshot_delay); + g_test_add_func ("/portal/screenshot/cancel", test_screenshot_cancel); + g_test_add_func ("/portal/screenshot/close", test_screenshot_close); + g_test_add_func ("/portal/screenshot/parallel", test_screenshot_parallel); + + g_test_add_func ("/portal/color/basic", test_color_basic); + g_test_add_func ("/portal/color/delay", test_color_delay); + g_test_add_func ("/portal/color/cancel", test_color_cancel); + g_test_add_func ("/portal/color/close", test_color_close); + g_test_add_func ("/portal/color/parallel", test_color_parallel); + + g_test_add_func ("/portal/trash/file", test_trash_file); + + g_test_add_func ("/portal/openfile/basic", test_open_file_basic); + g_test_add_func ("/portal/openfile/delay", test_open_file_delay); + g_test_add_func ("/portal/openfile/close", test_open_file_close); + g_test_add_func ("/portal/openfile/cancel", test_open_file_cancel); + g_test_add_func ("/portal/openfile/multiple", test_open_file_multiple); + g_test_add_func ("/portal/openfile/filters1", test_open_file_filters1); + g_test_add_func ("/portal/openfile/filters2", test_open_file_filters2); + g_test_add_func ("/portal/openfile/current_filter1", test_open_file_current_filter1); + g_test_add_func ("/portal/openfile/current_filter2", test_open_file_current_filter2); + g_test_add_func ("/portal/openfile/current_filter3", test_open_file_current_filter3); + g_test_add_func ("/portal/openfile/current_filter4", test_open_file_current_filter4); + g_test_add_func ("/portal/openfile/choices1", test_open_file_choices1); + g_test_add_func ("/portal/openfile/choices2", test_open_file_choices2); + g_test_add_func ("/portal/openfile/choices3", test_open_file_choices3); + g_test_add_func ("/portal/openfile/parallel", test_open_file_parallel); + + g_test_add_func ("/portal/savefile/basic", test_save_file_basic); + g_test_add_func ("/portal/savefile/delay", test_save_file_delay); + g_test_add_func ("/portal/savefile/close", test_save_file_close); + g_test_add_func ("/portal/savefile/cancel", test_save_file_cancel); + g_test_add_func ("/portal/savefile/filters", test_save_file_filters); + g_test_add_func ("/portal/savefile/lockdown", test_save_file_lockdown); + g_test_add_func ("/portal/savefile/parallel", test_save_file_parallel); + + g_test_add_func ("/portal/prepareprint/basic", test_prepare_print_basic); + g_test_add_func ("/portal/prepareprint/delay", test_prepare_print_delay); + g_test_add_func ("/portal/prepareprint/close", test_prepare_print_close); + g_test_add_func ("/portal/prepareprint/cancel", test_prepare_print_cancel); + g_test_add_func ("/portal/prepareprint/lockdown", test_prepare_print_lockdown); + g_test_add_func ("/portal/prepareprint/results", test_prepare_print_results); + g_test_add_func ("/portal/prepareprint/parallel", test_prepare_print_parallel); + + g_test_add_func ("/portal/print/basic", test_print_basic); + g_test_add_func ("/portal/print/delay", test_print_delay); + g_test_add_func ("/portal/print/close", test_print_close); + g_test_add_func ("/portal/print/cancel", test_print_cancel); + g_test_add_func ("/portal/print/lockdown", test_print_lockdown); + g_test_add_func ("/portal/print/parallel", test_print_parallel); + + g_test_add_func ("/portal/camera/basic", test_camera_basic); + g_test_add_func ("/portal/camera/delay", test_camera_delay); + g_test_add_func ("/portal/camera/close", test_camera_close); + g_test_add_func ("/portal/camera/cancel", test_camera_cancel); + g_test_add_func ("/portal/camera/lockdown", test_camera_lockdown); + g_test_add_func ("/portal/camera/noaccess1", test_camera_no_access1); + g_test_add_func ("/portal/camera/noaccess2", test_camera_no_access2); + g_test_add_func ("/portal/camera/parallel", test_camera_parallel); + + g_test_add_func ("/portal/inhibit/basic", test_inhibit_basic); + g_test_add_func ("/portal/inhibit/delay", test_inhibit_delay); + g_test_add_func ("/portal/inhibit/close", test_inhibit_close); + g_test_add_func ("/portal/inhibit/cancel", test_inhibit_cancel); + g_test_add_func ("/portal/inhibit/parallel", test_inhibit_parallel); + g_test_add_func ("/portal/inhibit/permissions", test_inhibit_permissions); + g_test_add_func ("/portal/inhibit/monitor", test_inhibit_monitor); + + g_test_add_func ("/portal/openuri/http", test_open_uri_http); + g_test_add_func ("/portal/openuri/http2", test_open_uri_http2); + g_test_add_func ("/portal/openuri/file", test_open_uri_file); + g_test_add_func ("/portal/openuri/delay", test_open_uri_delay); + g_test_add_func ("/portal/openuri/close", test_open_uri_close); + g_test_add_func ("/portal/openuri/cancel", test_open_uri_cancel); + g_test_add_func ("/portal/openuri/lockdown", test_open_uri_lockdown); + g_test_add_func ("/portal/openuri/directory", test_open_directory); + + g_test_add_func ("/portal/wallpaper/basic", test_wallpaper_basic); + g_test_add_func ("/portal/wallpaper/delay", test_wallpaper_delay); + g_test_add_func ("/portal/wallpaper/cancel1", test_wallpaper_cancel1); + g_test_add_func ("/portal/wallpaper/cancel2", test_wallpaper_cancel2); + g_test_add_func ("/portal/wallpaper/permission", test_wallpaper_permission); + + g_test_add_func ("/portal/location/basic", test_location_basic); + g_test_add_func ("/portal/location/accuracy", test_location_accuracy); + + g_test_add_func ("/portal/background/basic1", test_background_basic1); + g_test_add_func ("/portal/background/basic2", test_background_basic2); + g_test_add_func ("/portal/background/commandline", test_background_commandline); + g_test_add_func ("/portal/background/reason", test_background_reason); + + g_test_add_func ("/portal/notification/basic", test_notification_basic); + g_test_add_func ("/portal/notification/buttons", test_notification_buttons); + g_test_add_func ("/portal/notification/bad-arg", test_notification_bad_arg); + g_test_add_func ("/portal/notification/bad-priority", test_notification_bad_priority); + g_test_add_func ("/portal/notification/bad-button", test_notification_bad_button); +#endif + + global_setup (); + + res = g_test_run (); + + sleep (1); + + global_teardown (); + + return res; +} + diff --git a/tests/test-xdp-utils.c b/tests/test-xdp-utils.c new file mode 100644 index 0000000..531b240 --- /dev/null +++ b/tests/test-xdp-utils.c @@ -0,0 +1,199 @@ +#include "config.h" + +#include + +#include "xdp-utils.h" + +static void +test_parse_cgroup_unified (void) +{ + char data[] = "0::/user.slice/user-1000.slice/user@1000.service/apps.slice/snap.something.scope\n"; + FILE *f; + int res; + gboolean is_snap = FALSE; + + f = fmemopen(data, sizeof(data), "r"); + + res = _xdp_parse_cgroup_file (f, &is_snap); + g_assert_cmpint (res, ==, 0); + g_assert_true (is_snap); + fclose(f); +} + +static void +test_parse_cgroup_freezer (void) +{ + char data[] = + "12:pids:/user.slice/user-1000.slice/user@1000.service\n" + "11:perf_event:/\n" + "10:net_cls,net_prio:/\n" + "9:cpuset:/\n" + "8:memory:/user.slice/user-1000.slice/user@1000.service/apps.slice/apps-org.gnome.Terminal.slice/vte-spawn-228ae109-a869-4533-8988-65ea4c10b492.scope\n" + "7:rdma:/\n" + "6:devices:/user.slice\n" + "5:blkio:/user.slice\n" + "4:hugetlb:/\n" + "3:freezer:/snap.portal-test\n" + "2:cpu,cpuacct:/user.slice\n" + "1:name=systemd:/user.slice/user-1000.slice/user@1000.service/apps.slice/apps-org.gnome.Terminal.slice/vte-spawn-228ae109-a869-4533-8988-65ea4c10b492.scope\n" + "0::/user.slice/user-1000.slice/user@1000.service/apps.slice/apps-org.gnome.Terminal.slice/vte-spawn-228ae109-a869-4533-8988-65ea4c10b492.scope\n"; + FILE *f; + int res; + gboolean is_snap = FALSE; + + f = fmemopen(data, sizeof(data), "r"); + + res = _xdp_parse_cgroup_file (f, &is_snap); + g_assert_cmpint (res, ==, 0); + g_assert_true (is_snap); + fclose(f); +} + +static void +test_parse_cgroup_systemd (void) +{ + char data[] = "1:name=systemd:/user.slice/user-1000.slice/user@1000.service/apps.slice/snap.something.scope\n"; + FILE *f; + int res; + gboolean is_snap = FALSE; + + f = fmemopen(data, sizeof(data), "r"); + + res = _xdp_parse_cgroup_file (f, &is_snap); + g_assert_cmpint (res, ==, 0); + g_assert_true (is_snap); + fclose(f); +} + +static void +test_parse_cgroup_not_snap (void) +{ + char data[] = + "12:pids:/\n" + "11:perf_event:/\n" + "10:net_cls,net_prio:/\n" + "9:cpuset:/\n" + "8:memory:/\n" + "7:rdma:/\n" + "6:devices:/\n" + "5:blkio:/\n" + "4:hugetlb:/\n" + "3:freezer:/\n" + "2:cpu,cpuacct:/\n" + "1:name=systemd:/\n" + "0::/\n"; + + FILE *f; + int res; + gboolean is_snap = FALSE; + + f = fmemopen(data, sizeof(data), "r"); + + res = _xdp_parse_cgroup_file (f, &is_snap); + g_assert_cmpint (res, ==, 0); + g_assert_false (is_snap); + fclose(f); +} + +static void +test_alternate_doc_path (void) +{ + g_autofree char *path = NULL; + + xdp_set_documents_mountpoint (NULL); + + /* If no documents mount point is set, there is no alternate path */ + path = xdp_get_alternate_document_path ("/whatever", "app-id"); + g_assert_cmpstr (path, ==, NULL); + + xdp_set_documents_mountpoint ("/doc/portal"); + + /* Paths outside of the document portal do not have an alternate path */ + path = xdp_get_alternate_document_path ("/whatever", "app-id"); + g_assert_cmpstr (path, ==, NULL); + + /* The doc portal mount point itself does not have an alternate path */ + path = xdp_get_alternate_document_path ("/doc/portal", "app-id"); + g_assert_cmpstr (path, ==, NULL); + + /* Paths under the doc portal mount point have an alternate path */ + path = xdp_get_alternate_document_path ("/doc/portal/foo/bar", "app-id"); + g_assert_cmpstr (path, ==, "/doc/portal/by-app/app-id/foo/bar"); + + g_clear_pointer (&path, g_free); + path = xdp_get_alternate_document_path ("/doc/portal/foo/bar", "second-app"); + g_assert_cmpstr (path, ==, "/doc/portal/by-app/second-app/foo/bar"); + + xdp_set_documents_mountpoint (NULL); +} + +#ifdef HAVE_LIBSYSTEMD +static void +test_app_id_via_systemd_unit (void) +{ + g_autofree char *app_id = NULL; + + app_id = _xdp_parse_app_id_from_unit_name ("app-not-a-well-formed-unit-name"); + g_assert_cmpstr (app_id, ==, ""); + g_clear_pointer (&app_id, g_free); + + app_id = _xdp_parse_app_id_from_unit_name ("app-gnome-org.gnome.Evolution\\x2dalarm\\x2dnotify-2437.scope"); + /* Note, this is not Evolution's app ID, because the scope is for a background service */ + g_assert_cmpstr (app_id, ==, "org.gnome.Evolution-alarm-notify"); + g_clear_pointer (&app_id, g_free); + + app_id = _xdp_parse_app_id_from_unit_name ("app-gnome-org.gnome.Epiphany-182352.scope"); + g_assert_cmpstr (app_id, ==, "org.gnome.Epiphany"); + g_clear_pointer (&app_id, g_free); + + app_id = _xdp_parse_app_id_from_unit_name ("app-glib-spice\\x2dvdagent-1839.scope"); + g_assert_cmpstr (app_id, ==, "spice-vdagent"); + g_clear_pointer (&app_id, g_free); + + app_id = _xdp_parse_app_id_from_unit_name ("app-KDE-org.kde.okular@12345.service"); + g_assert_cmpstr (app_id, ==, "org.kde.okular"); + g_clear_pointer (&app_id, g_free); + + app_id = _xdp_parse_app_id_from_unit_name ("app-firefox.service"); + g_assert_cmpstr (app_id, ==, "firefox"); + g_clear_pointer (&app_id, g_free); + + app_id = _xdp_parse_app_id_from_unit_name ("app-org.kde.amarok.service"); + g_assert_cmpstr (app_id, ==, "org.kde.amarok"); + g_clear_pointer (&app_id, g_free); + + app_id = _xdp_parse_app_id_from_unit_name ("app-gnome-org.gnome.SettingsDaemon.DiskUtilityNotify-autostart.service"); + g_assert_cmpstr (app_id, ==, "org.gnome.SettingsDaemon.DiskUtilityNotify"); + g_clear_pointer (&app_id, g_free); + + app_id = _xdp_parse_app_id_from_unit_name ("app-gnome-org.gnome.Terminal-92502.slice"); + g_assert_cmpstr (app_id, ==, "org.gnome.Terminal"); + g_clear_pointer (&app_id, g_free); + + app_id = _xdp_parse_app_id_from_unit_name ("app-com.obsproject.Studio-d70acc38b5154a3a8b4a60accc4b15f4.scope"); + g_assert_cmpstr (app_id, ==, "com.obsproject.Studio"); + g_clear_pointer (&app_id, g_free); + + app_id = _xdp_parse_app_id_from_unit_name ("app-firefox-jcfppqx.scope"); + g_assert_cmpstr (app_id, ==, "firefox"); + g_clear_pointer (&app_id, g_free); + + app_id = _xdp_parse_app_id_from_unit_name ("app-gnome-firefox.service"); + g_assert_cmpstr (app_id, ==, "firefox"); + g_clear_pointer (&app_id, g_free); +} +#endif /* HAVE_LIBSYSTEMD */ + +int main (int argc, char **argv) +{ + g_test_init (&argc, &argv, NULL); + g_test_add_func ("/parse-cgroup/unified", test_parse_cgroup_unified); + g_test_add_func ("/parse-cgroup/freezer", test_parse_cgroup_freezer); + g_test_add_func ("/parse-cgroup/systemd", test_parse_cgroup_systemd); + g_test_add_func ("/parse-cgroup/not-snap", test_parse_cgroup_not_snap); + g_test_add_func ("/alternate-doc-path", test_alternate_doc_path); +#ifdef HAVE_LIBSYSTEMD + g_test_add_func ("/app-id-via-systemd-unit", test_app_id_via_systemd_unit); +#endif + return g_test_run (); +} diff --git a/tests/test_clipboard.py b/tests/test_clipboard.py new file mode 100644 index 0000000..e2db531 --- /dev/null +++ b/tests/test_clipboard.py @@ -0,0 +1,105 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is formatted with Python Black + +from tests import PortalMock, Session +import dbus +import pytest +import os + + +@pytest.fixture +def portal_name(): + return "Clipboard" + + +class TestClipboard: + def test_version(self, portal_mock): + portal_mock.check_version(1) + + def start_session(self, pmock, params={}): + pmock.start_impl_portal(params=params) + pmock.add_template(portal="RemoteDesktop", params=params) + pmock.start_xdp() + + create_session_request = pmock.create_request("RemoteDesktop") + create_session_response = create_session_request.call( + "CreateSession", options={"session_handle_token": "1234"} + ) + assert create_session_response.response == 0 + assert str(create_session_response.results["session_handle"]) + + session = Session.from_response(pmock.dbus_con, create_session_response) + + clipboard_interface = pmock.get_dbus_interface() + clipboard_interface.RequestClipboard(session.handle, {}) + + start_session_request = pmock.create_request("RemoteDesktop") + start_session_response = start_session_request.call( + "Start", session_handle=session.handle, parent_window="", options={} + ) + + assert start_session_response.response == 0 + + return (session, start_session_response.results.get("clipboard_enabled")) + + def test_request_clipboard_and_start_session(self, session_bus): + pmock = PortalMock(session_bus, "Clipboard") + params = {"force-clipboard-enabled": True} + _, clipboard_enabled = self.start_session(pmock, params) + + assert clipboard_enabled + + def test_clipboard_checks_clipboard_enabled(self, session_bus): + pmock = PortalMock(session_bus, "Clipboard") + session, clipboard_enabled = self.start_session(pmock) + clipboard_interface = pmock.get_dbus_interface() + + assert not clipboard_enabled + + with pytest.raises(dbus.exceptions.DBusException): + clipboard_interface.SetSelection(session.handle, {}) + + def test_clipboard_set_selection(self, session_bus): + pmock = PortalMock(session_bus, "Clipboard") + params = {"force-clipboard-enabled": True} + session, _ = self.start_session(pmock, params) + clipboard_interface = pmock.get_dbus_interface() + + clipboard_interface.SetSelection(session.handle, {}) + + def test_clipboard_selection_write(self, session_bus): + pmock = PortalMock(session_bus, "Clipboard") + params = {"force-clipboard-enabled": True} + session, _ = self.start_session(pmock, params) + clipboard_interface = pmock.get_dbus_interface() + + fd_object: dbus.types.UnixFd = clipboard_interface.SelectionWrite( + session.handle, 1234 + ) + assert fd_object + + fd = fd_object.take() + assert fd + + bytes_written = os.write(fd, b"Clipboard") + assert bytes_written > 0 + + clipboard_interface.SelectionWriteDone(session.handle, 1234, True) + + def test_clipboard_selection_read(self, session_bus): + pmock = PortalMock(session_bus, "Clipboard") + params = {"force-clipboard-enabled": True} + session, _ = self.start_session(pmock, params) + clipboard_interface = pmock.get_dbus_interface() + + fd_object: dbus.types.UnixFd = clipboard_interface.SelectionRead( + session.handle, "mimetype" + ) + assert fd_object + + fd = fd_object.take() + assert fd + + clipboard = os.read(fd, 1000) + assert str(clipboard) diff --git a/tests/test_email.py b/tests/test_email.py new file mode 100644 index 0000000..d15e101 --- /dev/null +++ b/tests/test_email.py @@ -0,0 +1,234 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is formatted with Python Black + +from tests import PortalMock +import dbus +import pytest +import time + + +@pytest.fixture +def portal_name(): + yield "Email" + + +@pytest.fixture +def portal_has_impl(): + yield True + + +class TestEmail: + def test_version(self, portal_mock): + portal_mock.check_version(3) + + def test_email_basic(self, portal_mock): + addresses = ["mclasen@redhat.com"] + subject = "Re: portal tests" + body = "You have to see this" + + request = portal_mock.create_request() + options = { + "addresses": addresses, + "subject": subject, + "body": body, + } + response = request.call( + "ComposeEmail", + parent_window="", + options=options, + ) + + assert response.response == 0 + + # Check the impl portal was called with the right args + method_calls = portal_mock.mock_interface.GetMethodCalls("ComposeEmail") + assert len(method_calls) > 0 + _, args = method_calls[-1] + assert args[2] == "" # parent window + assert args[3]["addresses"] == addresses + assert args[3]["subject"] == subject + assert args[3]["body"] == body + + def test_email_address(self, portal_mock): + """test that an invalid address triggers an error""" + + addresses = ["gibberish! not an email address\n%Q"] + + request = portal_mock.create_request() + options = { + "addresses": addresses, + } + try: + request.call( + "ComposeEmail", + parent_window="", + options=options, + ) + + assert False, "This statement should not be reached" + except dbus.exceptions.DBusException as e: + assert e.get_dbus_name() == "org.freedesktop.portal.Error.InvalidArgument" + + # Check the impl portal was never called + method_calls = portal_mock.mock_interface.GetMethodCalls("ComposeEmail") + assert len(method_calls) == 0 + + def test_email_subject_multiline(self, portal_mock): + """test that an multiline subject triggers an error""" + + subject = "not\na\nvalid\nsubject line" + + request = portal_mock.create_request() + options = { + "subject": subject, + } + try: + request.call( + "ComposeEmail", + parent_window="", + options=options, + ) + + assert False, "This statement should not be reached" + except dbus.exceptions.DBusException as e: + assert e.get_dbus_name() == "org.freedesktop.portal.Error.InvalidArgument" + + # Check the impl portal was never called + method_calls = portal_mock.mock_interface.GetMethodCalls("ComposeEmail") + assert len(method_calls) == 0 + + def test_email_subject_too_long(self, portal_mock): + """test that a subject line over 200 chars triggers an error""" + + subject = "This subject line is too long" + "abc" * 60 + + assert len(subject) > 200 + + request = portal_mock.create_request() + options = { + "subject": subject, + } + try: + request.call( + "ComposeEmail", + parent_window="", + options=options, + ) + + assert False, "This statement should not be reached" + except dbus.exceptions.DBusException as e: + assert e.get_dbus_name() == "org.freedesktop.portal.Error.InvalidArgument" + + # Check the impl portal was never called + method_calls = portal_mock.mock_interface.GetMethodCalls("ComposeEmail") + assert len(method_calls) == 0 + + @pytest.mark.parametrize("params", ({"delay": 2000},)) + def test_email_delay(self, portal_mock): + """ + Test that everything works as expected when the backend takes some + time to send its response, as * is to be expected from a real backend + that presents dialogs to the user. + """ + subject = "delay test" + addresses = ["mclasen@redhat.com"] + + request = portal_mock.create_request() + options = { + "addresses": addresses, + "subject": subject, + } + + start_time = time.perf_counter() + + response = request.call( + "ComposeEmail", + parent_window="", + options=options, + ) + + assert response.response == 0 + + end_time = time.perf_counter() + + assert end_time - start_time > 2 + + # Check the impl portal was called with the right args + method_calls = portal_mock.mock_interface.GetMethodCalls("ComposeEmail") + assert len(method_calls) > 0 + _, args = method_calls[-1] + assert args[2] == "" # parent window + assert args[3]["addresses"] == addresses + assert args[3]["subject"] == subject + + @pytest.mark.parametrize("params", ({"response": 1},)) + def test_email_cancel(self, portal_mock): + """ + Test that user cancellation works as expected. + We simulate that the user cancels a hypothetical dialog, + by telling the backend to return 1 as response code. + And we check that we get the expected G_IO_ERROR_CANCELLED. + """ + + subject = "cancel test" + addresses = ["mclasen@redhat.com"] + + request = portal_mock.create_request() + options = { + "addresses": addresses, + "subject": subject, + } + + response = request.call( + "ComposeEmail", + parent_window="", + options=options, + ) + + assert response.response == 1 + + # Check the impl portal was called with the right args + method_calls = portal_mock.mock_interface.GetMethodCalls("ComposeEmail") + assert len(method_calls) > 0 + _, args = method_calls[-1] + assert args[2] == "" # parent window + assert args[3]["addresses"] == addresses + assert args[3]["subject"] == subject + + @pytest.mark.parametrize("params", ({"expect-close": True},)) + def test_email_close(self, portal_mock): + """ + Test that app-side cancellation works as expected. + We cancel the cancellable while while the hypothetical + dialog is up, and tell the backend that it should + expect a Close call. We rely on the backend to + verify that that call actually happened. + """ + + subject = "close test" + addresses = ["mclasen@redhat.com"] + + request = portal_mock.create_request() + request.schedule_close(1000) + options = { + "addresses": addresses, + "subject": subject, + } + + request.call( + "ComposeEmail", + parent_window="", + options=options, + ) + + # Only true if the impl.Request was closed too + assert request.closed + + # Check the impl portal was called with the right args + method_calls = portal_mock.mock_interface.GetMethodCalls("ComposeEmail") + assert len(method_calls) > 0 + _, args = method_calls[-1] + assert args[2] == "" # parent window + assert args[3]["addresses"] == addresses + assert args[3]["subject"] == subject diff --git a/tests/test_globalshortcuts.py b/tests/test_globalshortcuts.py new file mode 100644 index 0000000..56349cb --- /dev/null +++ b/tests/test_globalshortcuts.py @@ -0,0 +1,217 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is formatted with Python Black + + +from tests import PortalMock, Session +from gi.repository import GLib + +import dbus +import pytest +import time + + +@pytest.fixture +def portal_name(): + return "GlobalShortcuts" + + +class TestGlobalShortcuts: + def test_version(self, portal_mock): + portal_mock.check_version(1) + + def test_global_shortcuts_create_close_session(self, portal_mock): + request = portal_mock.create_request() + options = { + "session_handle_token": "session_token0", + } + response = request.call( + "CreateSession", + options=options, + ) + + assert response.response == 0 + + session = Session.from_response(portal_mock.dbus_con, response) + # Check the impl portal was called with the right args + method_calls = portal_mock.mock_interface.GetMethodCalls("CreateSession") + assert len(method_calls) > 0 + _, args = method_calls[-1] + assert args[1] == session.handle + assert args[2] == "" # appid + + session.close() + + mainloop = GLib.MainLoop() + GLib.timeout_add(2000, mainloop.quit) + mainloop.run() + + assert session.closed + + @pytest.mark.parametrize("params", ({"force-close": 500},)) + def test_global_shortcuts_create_session_signal_closed(self, portal_mock): + request = portal_mock.create_request() + options = { + "session_handle_token": "session_token0", + } + response = request.call( + "CreateSession", + options=options, + ) + + assert response.response == 0 + + session = Session.from_response(portal_mock.dbus_con, response) + # Check the impl portal was called with the right args + method_calls = portal_mock.mock_interface.GetMethodCalls("CreateSession") + assert len(method_calls) > 0 + _, args = method_calls[-1] + assert args[1] == session.handle + assert args[2] == "" # appid + + # Now expect the backend to close it + + mainloop = GLib.MainLoop() + GLib.timeout_add(2000, mainloop.quit) + mainloop.run() + + assert session.closed + + def test_global_shortcuts_bind_list_shortcuts(self, portal_mock): + request = portal_mock.create_request() + options = { + "session_handle_token": "session_token0", + } + response = request.call( + "CreateSession", + options=options, + ) + + assert response.response == 0 + + session = Session.from_response(portal_mock.dbus_con, response) + + shortcuts = [ + ( + "binding1", + { + "description": dbus.String("Binding #1", variant_level=1), + "preferred-trigger": dbus.String("CTRL+a", variant_level=1), + }, + ), + ( + "binding2", + { + "description": dbus.String("Binding #2", variant_level=1), + "preferred-trigger": dbus.String("CTRL+b", variant_level=1), + }, + ), + ] + + request = portal_mock.create_request() + response = request.call( + "BindShortcuts", + session_handle=session.handle, + shortcuts=shortcuts, + parent_window="", + options={}, + ) + + request = portal_mock.create_request() + options = {} + response = request.call( + "ListShortcuts", + session_handle=session.handle, + options=options, + ) + + assert len(list(response.results["shortcuts"])) == len(list(shortcuts)) + + session.close() + + mainloop = GLib.MainLoop() + GLib.timeout_add(2000, mainloop.quit) + mainloop.run() + + assert session.closed + + def test_global_shortcuts_trigger(self, portal_mock): + request = portal_mock.create_request() + options = { + "session_handle_token": "session_token0", + } + response = request.call( + "CreateSession", + options=options, + ) + + assert response.response == 0 + + session = Session.from_response(portal_mock.dbus_con, response) + + shortcuts = [ + ( + "binding1", + { + "description": dbus.String("Binding #1", variant_level=1), + "preferred-trigger": dbus.String("CTRL+a", variant_level=1), + }, + ), + ] + + request = portal_mock.create_request() + response = request.call( + "BindShortcuts", + session_handle=session.handle, + shortcuts=shortcuts, + parent_window="", + options={}, + ) + + activated_count = 0 + deactivated_count = 0 + + def cb_activated(session_handle, shortcut_id, timestamp, options): + nonlocal activated_count + now_since_epoch = int(time.time() * 1000000) + # This assert will race twice a year on systems configured with + # summer time timezone changes + assert ( + now_since_epoch > timestamp + and (now_since_epoch - 10 * 10001000) < timestamp + ) + assert shortcut_id == "binding1" + activated_count += 1 + + def cb_deactivated(session_handle, shortcut_id, timestamp, options): + nonlocal deactivated_count + now_since_epoch = int(time.time() * 1000000) + # This assert will race twice a year on systems configured with + # summer time timezone changes + assert ( + now_since_epoch > timestamp + and (now_since_epoch - 10 * 10001000) < timestamp + ) + assert shortcut_id == "binding1" + deactivated_count += 1 + + gs_intf = portal_mock.get_dbus_interface() + gs_intf.connect_to_signal("Activated", cb_activated) + gs_intf.connect_to_signal("Deactivated", cb_deactivated) + + portal_mock.mock_interface.Trigger(session.handle, "binding1") + + mainloop = GLib.MainLoop() + GLib.timeout_add(2000, mainloop.quit) + mainloop.run() + + assert activated_count == 1 + assert deactivated_count == 1 + + session.close() + + mainloop = GLib.MainLoop() + GLib.timeout_add(2000, mainloop.quit) + mainloop.run() + + assert session.closed diff --git a/tests/test_inputcapture.py b/tests/test_inputcapture.py new file mode 100644 index 0000000..07c130d --- /dev/null +++ b/tests/test_inputcapture.py @@ -0,0 +1,616 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is formatted with Python Black + +from gi.repository import GLib + +from itertools import count + +import dbus +import pytest +import socket + +counter = count() + + +def default_zones(): + return [(1024, 768, 0, 0), (640, 480, 1024, 0)] + + +@pytest.fixture +def portal_name(): + return "InputCapture" + + +@pytest.fixture +def zones(): + return default_zones() + + +class TestInputCapture: + def create_session(self, portal_mock, capabilities=0xF): + """ + Call CreateSession for the given capabilities and return the + (response, results) tuple. + """ + request = portal_mock.create_request() + + capabilities = dbus.UInt32(capabilities, variant_level=1) + session_handle_token = dbus.String(f"session{next(counter)}", variant_level=1) + + options = dbus.Dictionary( + { + "capabilities": capabilities, + "session_handle_token": session_handle_token, + }, + signature="sv", + ) + + response, results = request.call( + "CreateSession", parent_window="", options=options + ) + assert response == 0 + assert "session_handle" in results + assert "capabilities" in results + caps = results["capabilities"] + # Returned capabilities must be a subset of the requested ones + assert caps & ~capabilities == 0 + + self.current_session_handle = results["session_handle"] + + # Check the impl portal was called with the right args + method_calls = portal_mock.mock_interface.GetMethodCalls("CreateSession") + assert len(method_calls) > 0 + _, args = method_calls[-1] + assert args[3] == "" # parent window + assert args[4]["capabilities"] == capabilities + + return response, results + + def get_zones(self, portal_mock): + """ + Call GetZones and return the (response, results) tuple. + """ + request = portal_mock.create_request() + options = {} + response, results = request.call( + "GetZones", session_handle=self.current_session_handle, options=options + ) + assert response == 0 + assert "zones" in results + assert "zone_set" in results + + self.current_zone_set = results["zone_set"] + + # Check the impl portal was called with the right args + method_calls = portal_mock.mock_interface.GetMethodCalls("GetZones") + assert len(method_calls) > 0 + _, args = method_calls[-1] + assert args[0] == request.handle + assert args[1] == self.current_session_handle + + return response, results + + def set_pointer_barriers(self, portal_mock, barriers): + request = portal_mock.create_request() + options = {} + response, results = request.call( + "SetPointerBarriers", + session_handle=self.current_session_handle, + options=options, + barriers=barriers, + zone_set=self.current_zone_set, + ) + assert response == 0 + assert "failed_barriers" in results + + # Check the impl portal was called with the right args + method_calls = portal_mock.mock_interface.GetMethodCalls("SetPointerBarriers") + assert len(method_calls) > 0 + _, args = method_calls[-1] + assert args[0] == request.handle + assert args[1] == self.current_session_handle + assert args[4] == barriers + assert args[5] == self.current_zone_set + + return response, results + + def connect_to_eis(self, portal_mock): + inputcapture_intf = portal_mock.get_dbus_interface() + fd = inputcapture_intf.ConnectToEIS( + self.current_session_handle, dbus.Dictionary({}, signature="sv") + ) + + # Our dbusmock template sends HELLO + eis_socket = socket.fromfd(fd.take(), socket.AF_UNIX, socket.SOCK_STREAM) + hello = eis_socket.recv(10) + assert hello == b"HELLO" + + method_calls = portal_mock.mock_interface.GetMethodCalls("ConnectToEIS") + assert len(method_calls) > 0 + _, args = method_calls[-1] + assert args[0] == self.current_session_handle + + return eis_socket + + def enable(self, portal_mock): + inputcapture_intf = portal_mock.get_dbus_interface() + inputcapture_intf.Enable( + self.current_session_handle, dbus.Dictionary({}, signature="sv") + ) + + method_calls = portal_mock.mock_interface.GetMethodCalls("Enable") + assert len(method_calls) > 0 + _, args = method_calls[-1] + assert args[0] == self.current_session_handle + + def disable(self, portal_mock): + inputcapture_intf = portal_mock.get_dbus_interface() + inputcapture_intf.Disable( + self.current_session_handle, dbus.Dictionary({}, signature="sv") + ) + + method_calls = portal_mock.mock_interface.GetMethodCalls("Disable") + assert len(method_calls) > 0 + _, args = method_calls[-1] + assert args[0] == self.current_session_handle + + def release(self, portal_mock, activation_id: int, cursor_position=None): + options = {"activation_id": dbus.UInt32(activation_id)} + if cursor_position: + options["cursor_position"] = dbus.Struct( + list(cursor_position), signature="dd", variant_level=1 + ) + + inputcapture_intf = portal_mock.get_dbus_interface() + inputcapture_intf.Release( + self.current_session_handle, dbus.Dictionary(options, signature="sv") + ) + + method_calls = portal_mock.mock_interface.GetMethodCalls("Release") + assert len(method_calls) > 0 + _, args = method_calls[-1] + assert args[0] == self.current_session_handle + assert "activation_id" in args[2] + aid = args[2]["activation_id"] + assert aid == activation_id + if cursor_position: + assert "cursor_position" in args[2] + pos = args[2]["cursor_position"] + assert pos == cursor_position + + def test_version(self, portal_mock): + portal_mock.check_version(1) + + @pytest.mark.parametrize( + "params", + ( + { + "supported_capabilities": 0b101, # KEYBOARD, POINTER, TOUCH + }, + ), + ) + def test_supported_capabilities(self, portal_mock): + properties_intf = portal_mock.get_dbus_interface( + "org.freedesktop.DBus.Properties" + ) + caps = properties_intf.Get( + "org.freedesktop.portal.InputCapture", "SupportedCapabilities" + ) + assert caps == 0b101 + + def test_create_session(self, portal_mock): + self.create_session(portal_mock, capabilities=0b1) # KEYBOARD + + # Check the impl portal was called with the right args + method_calls = portal_mock.mock_interface.GetMethodCalls("CreateSession") + assert len(method_calls) == 1 + _, args = method_calls.pop(0) + assert args[3] == "" # parent window + assert args[4]["capabilities"] == 0b1 + + @pytest.mark.parametrize( + "params", + ( + { + "capabilities": 0b110, # TOUCH, POINTER + "supported_capabilities": 0b111, # TOUCH, POINTER, KEYBOARD + }, + ), + ) + def test_create_session_limited_caps(self, portal_mock): + # Request more caps than are supported + response, results = self.create_session(portal_mock, capabilities=0b111) + caps = results["capabilities"] + # Returned capabilities must the ones we set up in the params + assert caps == 0b110 + + # Check the impl portal was called with the right args + method_calls = portal_mock.mock_interface.GetMethodCalls("CreateSession") + assert len(method_calls) == 1 + _, args = method_calls.pop(0) + assert args[3] == "" # parent window + assert args[4]["capabilities"] == 0b111 + + @pytest.mark.parametrize( + "params", + ( + { + "default-zone": dbus.Array( + [dbus.Struct(z, signature="uuii") for z in default_zones()], + signature="(uuii)", + variant_level=1, + ) + }, + ), + ) + def test_get_zones(self, portal_mock, zones): + response, results = self.create_session(portal_mock) + response, results = self.get_zones(portal_mock) + for z1, z2 in zip(results["zones"], zones): + assert z1 == z2 + + # Check the impl portal was called with the right args + method_calls = portal_mock.mock_interface.GetMethodCalls("CreateSession") + assert len(method_calls) == 1 + method_calls = portal_mock.mock_interface.GetMethodCalls("GetZones") + assert len(method_calls) == 1 + + @pytest.mark.parametrize( + "params", + ( + { + "default-zone": dbus.Array( + [dbus.Struct(z, signature="uuii") for z in default_zones()], + signature="(uuii)", + variant_level=1, + ) + }, + ), + ) + def test_set_pointer_barriers(self, portal_mock, zones): + response, results = self.create_session(portal_mock) + response, results = self.get_zones(portal_mock) + + barriers = [ + { + "barrier_id": dbus.UInt32(10, variant_level=1), + "position": dbus.Struct( + [0, 0, 0, 768], signature="iiii", variant_level=1 + ), + }, + { + "barrier_id": dbus.UInt32(11, variant_level=1), + "position": dbus.Struct( + [0, 0, 1024, 0], signature="iiii", variant_level=1 + ), + }, + { + "barrier_id": dbus.UInt32(12, variant_level=1), + "position": dbus.Struct( + [1024, 0, 1024, 768], signature="iiii", variant_level=1 + ), + }, + { + "barrier_id": dbus.UInt32(13, variant_level=1), + "position": dbus.Struct( + [0, 768, 1024, 768], signature="iiii", variant_level=1 + ), + }, + { + "barrier_id": dbus.UInt32(14, variant_level=1), + "position": dbus.Struct( + [100, 768, 500, 768], signature="iiii", variant_level=1 + ), + }, + { + "barrier_id": dbus.UInt32(15, variant_level=1), + "position": dbus.Struct( + [1024, 0, 1024, 480], signature="iiii", variant_level=1 + ), + }, + { + "barrier_id": dbus.UInt32(16, variant_level=1), + "position": dbus.Struct( + [1024 + 640, 0, 1024 + 640, 480], signature="iiii", variant_level=1 + ), + }, + # invalid ones + { + "barrier_id": dbus.UInt32(20, variant_level=1), + "position": dbus.Struct( + [0, 1, 3, 4], signature="iiii", variant_level=1 + ), + }, + { + "barrier_id": dbus.UInt32(21, variant_level=1), + "position": dbus.Struct( + [0, 1, 1024, 1], signature="iiii", variant_level=1 + ), + }, + { + "barrier_id": dbus.UInt32(22, variant_level=1), + "position": dbus.Struct( + [1, 0, 1, 768], signature="iiii", variant_level=1 + ), + }, + { + "barrier_id": dbus.UInt32(23, variant_level=1), + "position": dbus.Struct( + [1023, 0, 1023, 768], signature="iiii", variant_level=1 + ), + }, + { + "barrier_id": dbus.UInt32(24, variant_level=1), + "position": dbus.Struct( + [0, 0, 1050, 0], signature="iiii", variant_level=1 + ), + }, + ] + response, results = self.set_pointer_barriers(portal_mock, barriers=barriers) + failed_barriers = results["failed_barriers"] + assert all([id >= 20 for id in failed_barriers]) + + for id in [b["barrier_id"] for b in barriers if b["barrier_id"] >= 20]: + assert id in failed_barriers + + # Check the impl portal was called with the right args + method_calls = portal_mock.mock_interface.GetMethodCalls("CreateSession") + assert len(method_calls) == 1 + method_calls = portal_mock.mock_interface.GetMethodCalls("GetZones") + assert len(method_calls) == 1 + method_calls = portal_mock.mock_interface.GetMethodCalls("SetPointerBarriers") + assert len(method_calls) == 1 + _, args = method_calls.pop(0) + assert args[4] == barriers + assert args[5] == self.current_zone_set + + def test_connect_to_eis(self, portal_mock): + self.create_session(portal_mock) + self.get_zones(portal_mock) + + # The default zone is 1920x1080 + barriers = [ + { + "barrier_id": dbus.UInt32(10, variant_level=1), + "position": dbus.Struct( + [0, 0, 1920, 0], signature="iiii", variant_level=1 + ), + }, + ] + self.set_pointer_barriers(portal_mock, barriers) + + self.connect_to_eis(portal_mock) + + def test_enable_disable(self, portal_mock): + self.create_session(portal_mock) + self.create_session(portal_mock) + self.get_zones(portal_mock) + + # The default zone is 1920x1080 + barriers = [ + { + "barrier_id": dbus.UInt32(10, variant_level=1), + "position": dbus.Struct( + [0, 0, 1920, 0], signature="iiii", variant_level=1 + ), + }, + ] + self.set_pointer_barriers(portal_mock, barriers) + self.connect_to_eis( + portal_mock, + ) + + # Disable before enable should be a noop + self.disable( + portal_mock, + ) + method_calls = portal_mock.mock_interface.GetMethodCalls("Disable") + assert len(method_calls) == 1 + + self.enable( + portal_mock, + ) + method_calls = portal_mock.mock_interface.GetMethodCalls("Enable") + assert len(method_calls) == 1 + + self.disable( + portal_mock, + ) + method_calls = portal_mock.mock_interface.GetMethodCalls("Disable") + assert len(method_calls) == 2 + + @pytest.mark.parametrize( + "params", + ( + { + "disable-delay": 200, + }, + ), + ) + def test_disable_signal(self, portal_mock): + self.create_session(portal_mock) + self.get_zones(portal_mock) + # The default zone is 1920x1080 + barriers = [ + { + "barrier_id": dbus.UInt32(10, variant_level=1), + "position": dbus.Struct( + [0, 0, 1920, 0], signature="iiii", variant_level=1 + ), + }, + ] + self.set_pointer_barriers(portal_mock, barriers) + self.connect_to_eis(portal_mock) + + disabled_signal_received = False + + def cb_disabled(session_handle, options): + nonlocal disabled_signal_received + disabled_signal_received = True + assert session_handle == session_handle + + inputcapture_intf = portal_mock.get_dbus_interface() + inputcapture_intf.connect_to_signal("Disabled", cb_disabled) + + self.enable(portal_mock) + + mainloop = GLib.MainLoop() + GLib.timeout_add(500, mainloop.quit) + mainloop.run() + + assert disabled_signal_received + + @pytest.mark.parametrize( + "params", + ( + { + "activated-delay": 200, + "deactivated-delay": 300, + }, + ), + ) + def test_activated_signal(self, portal_mock): + self.create_session(portal_mock) + self.get_zones(portal_mock) + # The default zone is 1920x1080 + barriers = [ + { + "barrier_id": dbus.UInt32(10, variant_level=1), + "position": dbus.Struct( + [0, 0, 1920, 0], signature="iiii", variant_level=1 + ), + }, + ] + self.set_pointer_barriers(portal_mock, barriers) + self.connect_to_eis(portal_mock) + + disabled_signal_received = False + activated_signal_received = False + deactivated_signal_received = False + + def cb_disabled(session_handle, options): + nonlocal disabled_signal_received + disabled_signal_received = True + + def cb_activated(session_handle, options): + nonlocal activated_signal_received + activated_signal_received = True + assert session_handle == session_handle + assert "activation_id" in options + assert "barrier_id" in options + assert options["barrier_id"] == 10 # template uses first barrier + assert "cursor_position" in options + assert options["cursor_position"] == ( + 10.0, + 20.0, + ) # template uses x+10, y+20 of first barrier + + def cb_deactivated(session_handle, options): + nonlocal deactivated_signal_received + deactivated_signal_received = True + assert session_handle == session_handle + assert "activation_id" in options + assert "cursor_position" in options + assert options["cursor_position"] == ( + 10.0, + 20.0, + ) # template uses x+10, y+20 of first barrier + + inputcapture_intf = portal_mock.get_dbus_interface() + inputcapture_intf.connect_to_signal("Activated", cb_activated) + inputcapture_intf.connect_to_signal("Deactivated", cb_deactivated) + inputcapture_intf.connect_to_signal("Disabled", cb_disabled) + + self.enable(portal_mock) + + mainloop = GLib.MainLoop() + GLib.timeout_add(500, mainloop.quit) + mainloop.run() + + assert activated_signal_received + assert deactivated_signal_received + assert not disabled_signal_received + + # Disabling should not trigger the signal + self.disable(portal_mock) + + mainloop = GLib.MainLoop() + GLib.timeout_add(500, mainloop.quit) + mainloop.run() + + assert not disabled_signal_received + + @pytest.mark.parametrize( + "params", + ( + { + "activated-delay": 200, + "deactivated-delay": 1000, + "disabled-delay": 1200, + }, + ), + ) + def test_release(self, portal_mock): + self.create_session(portal_mock) + self.get_zones(portal_mock) + # The default zone is 1920x1080 + barriers = [ + { + "barrier_id": dbus.UInt32(10, variant_level=1), + "position": dbus.Struct( + [0, 0, 1920, 0], signature="iiii", variant_level=1 + ), + }, + ] + self.set_pointer_barriers(portal_mock, barriers) + self.connect_to_eis(portal_mock) + + disabled_signal_received = False + activated_signal_received = False + deactivated_signal_received = False + activation_id = None + + def cb_disabled(session_handle, options): + nonlocal disabled_signal_received + disabled_signal_received = True + + def cb_activated(session_handle, options): + nonlocal activated_signal_received, activation_id + activated_signal_received = True + activation_id = options["activation_id"] + + def cb_deactivated(session_handle, options): + nonlocal deactivated_signal_received + deactivated_signal_received = True + + inputcapture_intf = portal_mock.get_dbus_interface() + inputcapture_intf.connect_to_signal("Disabled", cb_activated) + inputcapture_intf.connect_to_signal("Activated", cb_activated) + inputcapture_intf.connect_to_signal("Deactivated", cb_deactivated) + + self.enable(portal_mock) + + mainloop = GLib.MainLoop() + GLib.timeout_add(300, mainloop.quit) + mainloop.run() + + assert activated_signal_received + assert activation_id is not None + assert not deactivated_signal_received + assert not disabled_signal_received + + self.release( + portal_mock, cursor_position=(10.0, 50.0), activation_id=activation_id + ) + + # XDP should filter any signals the implementation may + # send after Release(). + + mainloop = GLib.MainLoop() + GLib.timeout_add(1000, mainloop.quit) + mainloop.run() + + # Release() implies deactivated + assert not deactivated_signal_received + assert not disabled_signal_received diff --git a/tests/test_remotedesktop.py b/tests/test_remotedesktop.py new file mode 100644 index 0000000..49b6b18 --- /dev/null +++ b/tests/test_remotedesktop.py @@ -0,0 +1,220 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is formatted with Python Black + + +from tests import PortalMock, Session +from gi.repository import GLib + +import dbus +import pytest +import socket + + +@pytest.fixture +def portal_name(): + return "RemoteDesktop" + + +class TestRemoteDesktop: + def test_version(self, portal_mock): + portal_mock.check_version(2) + + def test_remote_desktop_create_close_session(self, portal_mock): + request = portal_mock.create_request() + options = { + "session_handle_token": "session_token0", + } + response = request.call( + "CreateSession", + options=options, + ) + + assert response.response == 0 + + session = Session.from_response(portal_mock.dbus_con, response) + # Check the impl portal was called with the right args + method_calls = portal_mock.mock_interface.GetMethodCalls("CreateSession") + assert len(method_calls) > 0 + _, args = method_calls[-1] + assert args[1] == session.handle + assert args[2] == "" # appid + + session.close() + + mainloop = GLib.MainLoop() + GLib.timeout_add(2000, mainloop.quit) + mainloop.run() + + assert session.closed + + @pytest.mark.parametrize("params", ({"force-close": 500},)) + def test_remote_desktop_create_session_signal_closed(self, portal_mock): + request = portal_mock.create_request() + options = { + "session_handle_token": "session_token0", + } + response = request.call( + "CreateSession", + options=options, + ) + + assert response.response == 0 + + session = Session.from_response(portal_mock.dbus_con, response) + # Check the impl portal was called with the right args + method_calls = portal_mock.mock_interface.GetMethodCalls("CreateSession") + assert len(method_calls) > 0 + _, args = method_calls[-1] + assert args[1] == session.handle + assert args[2] == "" # appid + + # Now expect the backend to close it + + mainloop = GLib.MainLoop() + GLib.timeout_add(2000, mainloop.quit) + mainloop.run() + + assert session.closed + + def test_remote_desktop_connect_to_eis(self, portal_mock): + request = portal_mock.create_request() + options = { + "session_handle_token": "session_token0", + } + response = request.call( + "CreateSession", + options=options, + ) + + assert response.response == 0 + + session = Session.from_response(portal_mock.dbus_con, response) + request = portal_mock.create_request() + options = { + "types": dbus.UInt32(0x3), + } + response = request.call( + "SelectDevices", + session_handle=session.handle, + options=options, + ) + assert response.response == 0 + + request = portal_mock.create_request() + options = {} + response = request.call( + "Start", + session_handle=session.handle, + parent_window="", + options=options, + ) + assert response.response == 0 + + rd_intf = portal_mock.get_dbus_interface() + fd = rd_intf.ConnectToEIS(session.handle, dbus.Dictionary({}, signature="sv")) + eis_socket = socket.fromfd(fd.take(), socket.AF_UNIX, socket.SOCK_STREAM) + assert eis_socket.recv(10) == b"HELLO" + + @pytest.mark.parametrize("params", ({"fail-connect-to-eis": True},)) + def test_remote_desktop_connect_to_eis_fail(self, portal_mock): + request = portal_mock.create_request() + options = { + "session_handle_token": "session_token0", + } + response = request.call( + "CreateSession", + options=options, + ) + + assert response.response == 0 + + session = Session.from_response(portal_mock.dbus_con, response) + request = portal_mock.create_request() + options = { + "types": dbus.UInt32(0x3), + } + response = request.call( + "SelectDevices", + session_handle=session.handle, + options=options, + ) + assert response.response == 0 + + request = portal_mock.create_request() + options = {} + response = request.call( + "Start", + session_handle=session.handle, + parent_window="", + options=options, + ) + assert response.response == 0 + + with pytest.raises(dbus.exceptions.DBusException) as excinfo: + rd_intf = portal_mock.get_dbus_interface() + _ = rd_intf.ConnectToEIS( + session.handle, dbus.Dictionary({}, signature="sv") + ) + assert "Purposely failing ConnectToEIS" in excinfo.value.get_dbus_message() + + def test_remote_desktop_connect_to_eis_fail_notifies(self, portal_mock): + request = portal_mock.create_request() + options = { + "session_handle_token": "session_token0", + } + response = request.call( + "CreateSession", + options=options, + ) + + assert response.response == 0 + + session = Session.from_response(portal_mock.dbus_con, response) + request = portal_mock.create_request() + options = { + "types": dbus.UInt32(0x3), + } + response = request.call( + "SelectDevices", + session_handle=session.handle, + options=options, + ) + assert response.response == 0 + + request = portal_mock.create_request() + options = {} + response = request.call( + "Start", + session_handle=session.handle, + parent_window="", + options=options, + ) + assert response.response == 0 + + for notifyfunc in [ + {"name": "NotifyPointerMotion", "args": (1, 2)}, + {"name": "NotifyPointerMotionAbsolute", "args": (0, 1, 2)}, + {"name": "NotifyPointerButton", "args": (1, 1)}, + {"name": "NotifyPointerAxis", "args": (1, 1)}, + {"name": "NotifyPointerAxisDiscrete", "args": (1, 1)}, + {"name": "NotifyKeyboardKeycode", "args": (1, 1)}, + {"name": "NotifyKeyboardKeysym", "args": (1, 1)}, + {"name": "NotifyTouchDown", "args": (0, 0, 1, 1)}, + {"name": "NotifyTouchMotion", "args": (0, 0, 1, 1)}, + {"name": "NotifyTouchUp", "args": (0,)}, + ]: + with pytest.raises(dbus.exceptions.DBusException) as excinfo: + rd_intf = portal_mock.get_dbus_interface() + func = getattr(rd_intf, notifyfunc["name"]) + assert func is not None + func( + session.handle, + dbus.Dictionary({}, signature="sv"), + *notifyfunc["args"] + ) + # Not the best error message but... + assert ( + "Session is not allowed to call Notify" + in excinfo.value.get_dbus_message() + ) diff --git a/tests/test_trash.py b/tests/test_trash.py new file mode 100644 index 0000000..d745cd2 --- /dev/null +++ b/tests/test_trash.py @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is formatted with Python Black + + +from pathlib import Path + +import os +import pytest +import tempfile + + +@pytest.fixture +def portal_name(): + return "Trash" + + +@pytest.fixture +def portal_has_impl(): + return False + + +class TestTrash: + def test_version(self, portal_mock): + portal_mock.check_version(1) + + def test_trash_file_fails(self, portal_mock): + trash_intf = portal_mock.get_dbus_interface() + with open("/proc/cmdline") as fd: + result = trash_intf.TrashFile(fd.fileno()) + + assert result == 0 + + def test_trash_file(self, portal_mock): + trash_intf = portal_mock.get_dbus_interface() + + fd, name = tempfile.mkstemp(prefix="trash_portal_mock_", dir=Path.home()) + result = trash_intf.TrashFile(fd) + if result != 1: + os.unlink(name) + assert result == 1 + assert not Path(name).exists() diff --git a/tests/testdb.c b/tests/testdb.c new file mode 100644 index 0000000..67f7411 --- /dev/null +++ b/tests/testdb.c @@ -0,0 +1,360 @@ +#include "config.h" + +#include +#include + +/* +static void +dump_db (PermissionDb *db) +{ + g_autofree char *s = permission_db_print (db); + g_printerr ("\n%s\n", s); +} +*/ + +static PermissionDb * +create_test_db (gboolean serialized) +{ + PermissionDb *db; + + g_autoptr(PermissionDbEntry) entry1 = NULL; + g_autoptr(PermissionDbEntry) entry2 = NULL; + g_autoptr(PermissionDbEntry) entry3 = NULL; + g_autoptr(PermissionDbEntry) entry4 = NULL; + g_autoptr(PermissionDbEntry) entry5 = NULL; + g_autoptr(PermissionDbEntry) entry6 = NULL; + g_autoptr(PermissionDbEntry) entry7 = NULL; + GError *error = NULL; + const char *permissions1[] = { "read", "write", NULL }; + const char *permissions2[] = { "read", NULL }; + const char *permissions3[] = { "write", NULL }; + + db = permission_db_new (NULL, FALSE, &error); + g_assert_no_error (error); + g_assert (db != NULL); + + { + g_auto(GStrv) ids = permission_db_list_ids (db); + g_assert (ids != NULL); + g_assert (ids[0] == NULL); + } + + { + g_auto(GStrv) apps = permission_db_list_apps (db); + g_assert (apps != NULL); + g_assert (apps[0] == NULL); + } + + entry1 = permission_db_entry_new (g_variant_new_string ("foo-data")); + entry2 = permission_db_entry_set_app_permissions (entry1, "org.test.bapp", permissions2); + entry3 = permission_db_entry_set_app_permissions (entry2, "org.test.app", permissions1); + entry4 = permission_db_entry_set_app_permissions (entry3, "org.test.capp", permissions1); + + permission_db_set_entry (db, "foo", entry4); + + entry5 = permission_db_entry_new (g_variant_new_string ("bar-data")); + entry6 = permission_db_entry_set_app_permissions (entry5, "org.test.app", permissions2); + entry7 = permission_db_entry_set_app_permissions (entry6, "org.test.dapp", permissions3); + + permission_db_set_entry (db, "bar", entry7); + + if (serialized) + permission_db_update (db); + + return db; +} + +static void +verify_test_db (PermissionDb *db) +{ + g_auto(GStrv) ids; + g_autofree const char **apps1 = NULL; + g_autofree const char **apps2 = NULL; + g_auto(GStrv) all_apps = NULL; + + ids = permission_db_list_ids (db); + g_assert (g_strv_length (ids) == 2); + g_assert (g_strv_contains ((const char **) ids, "foo")); + g_assert (g_strv_contains ((const char **) ids, "bar")); + + { + g_autoptr(PermissionDbEntry) entry = NULL; + g_autofree const char **permissions1 = NULL; + g_autofree const char **permissions2 = NULL; + g_autofree const char **permissions3 = NULL; + g_autofree const char **permissions4 = NULL; + g_autoptr(GVariant) data1 = NULL; + + entry = permission_db_lookup (db, "foo"); + g_assert (entry != NULL); + data1 = permission_db_entry_get_data (entry); + g_assert (data1 != NULL); + g_assert_cmpstr (g_variant_get_type_string (data1), ==, "s"); + g_assert_cmpstr (g_variant_get_string (data1, NULL), ==, "foo-data"); + apps1 = permission_db_entry_list_apps (entry); + g_assert (g_strv_length ((char **) apps1) == 3); + g_assert (g_strv_contains (apps1, "org.test.app")); + g_assert (g_strv_contains (apps1, "org.test.bapp")); + g_assert (g_strv_contains (apps1, "org.test.capp")); + permissions1 = permission_db_entry_list_permissions (entry, "org.test.app"); + g_assert (g_strv_length ((char **) permissions1) == 2); + g_assert (g_strv_contains (permissions1, "read")); + g_assert (g_strv_contains (permissions1, "write")); + permissions2 = permission_db_entry_list_permissions (entry, "org.test.bapp"); + g_assert (g_strv_length ((char **) permissions2) == 1); + g_assert (g_strv_contains (permissions2, "read")); + permissions3 = permission_db_entry_list_permissions (entry, "org.test.capp"); + g_assert (g_strv_length ((char **) permissions3) == 2); + g_assert (g_strv_contains (permissions3, "read")); + g_assert (g_strv_contains (permissions3, "write")); + permissions4 = permission_db_entry_list_permissions (entry, "org.test.noapp"); + g_assert (permissions4 != NULL); + g_assert (g_strv_length ((char **) permissions4) == 0); + } + + { + g_autoptr(PermissionDbEntry) entry = NULL; + g_autofree const char **permissions5 = NULL; + g_autofree const char **permissions6 = NULL; + g_autoptr(GVariant) data2 = NULL; + + entry = permission_db_lookup (db, "bar"); + g_assert (entry != NULL); + data2 = permission_db_entry_get_data (entry); + g_assert (data2 != NULL); + g_assert_cmpstr (g_variant_get_type_string (data2), ==, "s"); + g_assert_cmpstr (g_variant_get_string (data2, NULL), ==, "bar-data"); + apps2 = permission_db_entry_list_apps (entry); + g_assert (g_strv_length ((char **) apps2) == 2); + g_assert (g_strv_contains (apps2, "org.test.app")); + g_assert (g_strv_contains (apps2, "org.test.dapp")); + permissions5 = permission_db_entry_list_permissions (entry, "org.test.app"); + g_assert (g_strv_length ((char **) permissions5) == 1); + g_assert (g_strv_contains (permissions5, "read")); + permissions6 = permission_db_entry_list_permissions (entry, "org.test.dapp"); + g_assert (g_strv_length ((char **) permissions6) == 1); + g_assert (g_strv_contains (permissions6, "write")); + } + + { + g_autoptr(PermissionDbEntry) entry = NULL; + entry = permission_db_lookup (db, "gazonk"); + g_assert (entry == NULL); + } + + all_apps = permission_db_list_apps (db); + g_assert (g_strv_length (all_apps) == 4); + g_assert (g_strv_contains ((const char **) all_apps, "org.test.app")); + g_assert (g_strv_contains ((const char **) all_apps, "org.test.bapp")); + g_assert (g_strv_contains ((const char **) all_apps, "org.test.capp")); + g_assert (g_strv_contains ((const char **) all_apps, "org.test.dapp")); +} + +static void +test_db_open (void) +{ + GError *error = NULL; + PermissionDb *db; + + db = permission_db_new (g_test_get_filename (G_TEST_DIST, "dbs", "does_not_exist", NULL), TRUE, &error); + g_assert (g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT) || + g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)); + g_assert (db == NULL); + g_clear_error (&error); + + db = permission_db_new (g_test_get_filename (G_TEST_DIST, "dbs", "does_not_exist", NULL), FALSE, &error); + g_assert_no_error (error); + g_assert (db != NULL); + g_clear_error (&error); + g_object_unref (db); + + db = permission_db_new (g_test_get_filename (G_TEST_DIST, "dbs", "no_tables", NULL), TRUE, &error); + g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL); + g_assert (db == NULL); + g_clear_error (&error); +} + +static void +test_serialize (void) +{ + g_autoptr(PermissionDb) db = NULL; + g_autoptr(PermissionDb) db2 = NULL; + g_autofree char *dump1 = NULL; + g_autofree char *dump2 = NULL; + g_autofree char *dump3 = NULL; + GError *error = NULL; + char tmpfile[] = "/tmp/testdbXXXXXX"; + int fd; + + db = create_test_db (FALSE); + + verify_test_db (db); + + dump1 = permission_db_print (db); + + g_assert (permission_db_is_dirty (db)); + + permission_db_update (db); + + verify_test_db (db); + + g_assert (!permission_db_is_dirty (db)); + + dump2 = permission_db_print (db); + + g_assert_cmpstr (dump1, ==, dump2); + + fd = g_mkstemp (tmpfile); + close (fd); + + permission_db_set_path (db, tmpfile); + + permission_db_save_content (db, &error); + g_assert_no_error (error); + + db2 = permission_db_new (tmpfile, TRUE, &error); + g_assert_no_error (error); + g_assert (db2 != NULL); + + dump3 = permission_db_print (db2); + + g_assert_cmpstr (dump1, ==, dump3); + + unlink (tmpfile); +} + +static void +test_modify (void) +{ + g_autoptr(PermissionDb) db = NULL; + const char *permissions[] = { "read", "write", "execute", NULL }; + const char *no_permissions[] = { NULL }; + + db = create_test_db (FALSE); + + /* Add permission */ + { + g_autoptr(PermissionDbEntry) entry1 = NULL; + g_autoptr(PermissionDbEntry) entry2 = NULL; + + entry1 = permission_db_lookup (db, "foo"); + entry2 = permission_db_entry_set_app_permissions (entry1, "org.test.app", permissions); + permission_db_set_entry (db, "foo", entry2); + } + + /* Add entry */ + { + g_autoptr(PermissionDbEntry) entry1 = NULL; + g_autoptr(PermissionDbEntry) entry2 = NULL; + + entry1 = permission_db_entry_new (g_variant_new_string ("gazonk-data")); + entry2 = permission_db_entry_set_app_permissions (entry1, "org.test.eapp", permissions); + permission_db_set_entry (db, "gazonk", entry2); + } + + /* Remove permission */ + { + g_autoptr(PermissionDbEntry) entry1 = NULL; + g_autoptr(PermissionDbEntry) entry2 = NULL; + + entry1 = permission_db_lookup (db, "bar"); + entry2 = permission_db_entry_set_app_permissions (entry1, "org.test.dapp", no_permissions); + permission_db_set_entry (db, "bar", entry2); + } + + /* Verify */ + { + g_autoptr(PermissionDbEntry) entry5 = NULL; + g_autoptr(PermissionDbEntry) entry6 = NULL; + g_autoptr(PermissionDbEntry) entry7 = NULL; + g_autofree const char **apps2 = NULL; + g_auto(GStrv) apps3 = NULL; + g_autofree const char **permissions1 = NULL; + g_autofree const char **permissions2 = NULL; + g_autofree const char **permissions3 = NULL; + + entry5 = permission_db_lookup (db, "foo"); + permissions1 = permission_db_entry_list_permissions (entry5, "org.test.app"); + g_assert (g_strv_length ((char **) permissions1) == 3); + g_assert (g_strv_contains (permissions1, "read")); + g_assert (g_strv_contains (permissions1, "write")); + g_assert (g_strv_contains (permissions1, "execute")); + + entry6 = permission_db_lookup (db, "bar"); + permissions2 = permission_db_entry_list_permissions (entry6, "org.test.dapp"); + g_assert (g_strv_length ((char **) permissions2) == 0); + + entry7 = permission_db_lookup (db, "gazonk"); + permissions3 = permission_db_entry_list_permissions (entry7, "org.test.eapp"); + g_assert (g_strv_length ((char **) permissions3) == 3); + g_assert (g_strv_contains (permissions3, "read")); + g_assert (g_strv_contains (permissions3, "write")); + g_assert (g_strv_contains (permissions3, "execute")); + + apps2 = permission_db_entry_list_apps (entry6); + g_assert_cmpint (g_strv_length ((char **) apps2), ==, 1); + g_assert (g_strv_contains (apps2, "org.test.app")); + + apps3 = permission_db_list_apps (db); + g_assert_cmpint (g_strv_length (apps3), ==, 4); + g_assert (g_strv_contains ((const char **) apps3, "org.test.app")); + g_assert (g_strv_contains ((const char **) apps3, "org.test.bapp")); + g_assert (g_strv_contains ((const char **) apps3, "org.test.capp")); + g_assert (g_strv_contains ((const char **) apps3, "org.test.eapp")); + } + + permission_db_update (db); + + /* Verify after serialize */ + { + g_autoptr(PermissionDbEntry) entry5 = NULL; + g_autoptr(PermissionDbEntry) entry6 = NULL; + g_autoptr(PermissionDbEntry) entry7 = NULL; + g_autofree const char **apps2 = NULL; + g_auto(GStrv) apps3 = NULL; + g_autofree const char **permissions1 = NULL; + g_autofree const char **permissions2 = NULL; + g_autofree const char **permissions3 = NULL; + + entry5 = permission_db_lookup (db, "foo"); + permissions1 = permission_db_entry_list_permissions (entry5, "org.test.app"); + g_assert (g_strv_length ((char **) permissions1) == 3); + g_assert (g_strv_contains (permissions1, "read")); + g_assert (g_strv_contains (permissions1, "write")); + g_assert (g_strv_contains (permissions1, "execute")); + + entry6 = permission_db_lookup (db, "bar"); + permissions2 = permission_db_entry_list_permissions (entry6, "org.test.dapp"); + g_assert (g_strv_length ((char **) permissions2) == 0); + + entry7 = permission_db_lookup (db, "gazonk"); + permissions3 = permission_db_entry_list_permissions (entry7, "org.test.eapp"); + g_assert (g_strv_length ((char **) permissions3) == 3); + g_assert (g_strv_contains (permissions3, "read")); + g_assert (g_strv_contains (permissions3, "write")); + g_assert (g_strv_contains (permissions3, "execute")); + + apps2 = permission_db_entry_list_apps (entry6); + g_assert_cmpint (g_strv_length ((char **) apps2), ==, 1); + g_assert (g_strv_contains (apps2, "org.test.app")); + + apps3 = permission_db_list_apps (db); + g_assert_cmpint (g_strv_length (apps3), ==, 4); + g_assert (g_strv_contains ((const char **) apps3, "org.test.app")); + g_assert (g_strv_contains ((const char **) apps3, "org.test.bapp")); + g_assert (g_strv_contains ((const char **) apps3, "org.test.capp")); + g_assert (g_strv_contains ((const char **) apps3, "org.test.eapp")); + } +} + +int +main (int argc, char **argv) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/db/open", test_db_open); + g_test_add_func ("/db/serialize", test_serialize); + g_test_add_func ("/db/modify", test_modify); + + return g_test_run (); +} diff --git a/tests/trash.c b/tests/trash.c new file mode 100644 index 0000000..5dc945f --- /dev/null +++ b/tests/trash.c @@ -0,0 +1,47 @@ +#include + +#include "trash.h" + +#include + +static gboolean got_info; + +static void +trash_cb (GObject *object, + GAsyncResult *result, + gpointer data) +{ + XdpPortal *portal = XDP_PORTAL (object); + gboolean expected = *(gboolean*)data; + gboolean ret; + g_autoptr(GError) error = NULL; + + ret = xdp_portal_trash_file_finish (portal, result, &error); + g_assert_cmpint (ret, ==, expected); + if (ret) + g_assert_no_error (error); + else + g_assert (error != NULL); + + got_info = TRUE; + g_main_context_wakeup (NULL); +} + +/* Reliably testing successful trashing in a CI environment + * is hard, so just test something that is sure to fail. + */ +void +test_trash_file (void) +{ + g_autoptr(XdpPortal) portal = NULL; + gboolean expected; + + portal = xdp_portal_new (); + + expected = FALSE; + got_info = FALSE; + xdp_portal_trash_file (portal, "/proc/cmdline", NULL, trash_cb, &expected); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} diff --git a/tests/trash.h b/tests/trash.h new file mode 100644 index 0000000..630768e --- /dev/null +++ b/tests/trash.h @@ -0,0 +1,3 @@ +#pragma once + +void test_trash_file (void); diff --git a/tests/utils.c b/tests/utils.c new file mode 100644 index 0000000..ebe15ce --- /dev/null +++ b/tests/utils.c @@ -0,0 +1,61 @@ +#include + +#include "utils.h" + +#include + +/* + * Set a property. Unlike gdbus-codegen-generated wrapper functions, this + * waits for the property change to take effect. + * + * If @value is floating, ownership is taken. + */ +gboolean +tests_set_property_sync (GDBusProxy *proxy, + const char *iface, + const char *property, + GVariant *value, + GError **error) +{ + g_autoptr (GVariant) res = NULL; + + res = g_dbus_proxy_call_sync (proxy, + "org.freedesktop.DBus.Properties.Set", + g_variant_new ("(ssv)", iface, property, value), + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + error); + return (res != NULL); +} + +/* We need this to ensure that dbus-daemon launched by GTestDBus is not + * causing our tests to hang (see GNOME/glib#2537), so we are redirecting + * all its output to stderr, while reading its pid and address to manage it. + * As bonus point, now the services output will be visible in test logs. + * This can be removed once GNOME/glib!2354 will be available everywhere. + */ +void +setup_dbus_daemon_wrapper (const char *outdir) +{ + g_autofree gchar *file_name = NULL; + g_autofree gchar *test_path = NULL; + g_autoptr (GError) error = NULL; + const char dbus_daemon_script[] = \ + "#!/usr/bin/env bash\n" + "export PATH=\"$ORIGINAL_PATH\"\n" + "\n" + "[[ \" ${@} \" =~ \" --print-address=\"[0-9]+\" \" ]] && " + " exec dbus-daemon \"$@\"\n" + "\n" + "exec dbus-daemon \"$@\" --print-address=959 959<&1 1>&2\n"; + + test_path = g_strjoin (":", outdir, g_getenv ("PATH"), NULL); + g_setenv ("ORIGINAL_PATH", g_getenv ("PATH"), TRUE); + g_setenv ("PATH", test_path, TRUE); + + file_name = g_build_filename (outdir, "dbus-daemon", NULL); + g_file_set_contents (file_name, dbus_daemon_script, -1, &error); + g_chmod (file_name, 0700); + g_assert_no_error (error); +} diff --git a/tests/utils.h b/tests/utils.h new file mode 100644 index 0000000..fae5d85 --- /dev/null +++ b/tests/utils.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +gboolean tests_set_property_sync (GDBusProxy *proxy, + const char *iface, + const char *property, + GVariant *value, + GError **error); + +void setup_dbus_daemon_wrapper (const char *outdir); diff --git a/tests/wallpaper.c b/tests/wallpaper.c new file mode 100644 index 0000000..d6fdc18 --- /dev/null +++ b/tests/wallpaper.c @@ -0,0 +1,294 @@ +#include + +#include "account.h" + +#include +#include "xdp-impl-dbus.h" + +extern char outdir[]; + +static int got_info = 0; + +extern XdpDbusImplPermissionStore *permission_store; + +static void +set_wallpaper_permissions (const char *permission) +{ + const char *permissions[2] = { NULL, NULL }; + g_autoptr(GError) error = NULL; + + permissions[0] = permission; + xdp_dbus_impl_permission_store_call_set_permission_sync (permission_store, + "wallpaper", + TRUE, + "wallpaper", + "", + permissions, + NULL, + &error); + g_assert_no_error (error); +} + +static void +reset_wallpaper_permissions (void) +{ + set_wallpaper_permissions (NULL); +} + +static void +wallpaper_cb (GObject *obj, + GAsyncResult *result, + gpointer data) +{ + XdpPortal *portal = XDP_PORTAL (obj); + g_autoptr(GError) error = NULL; + GKeyFile *keyfile = data; + gboolean res; + int response; + + response = g_key_file_get_integer (keyfile, "result", "response", NULL); + + res = xdp_portal_set_wallpaper_finish (portal, result, &error); + if (response == 0) + { + g_assert_true (res); + g_assert_no_error (error); + } + else if (response == 1) + { + g_assert_false (res); + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED); + } + else if (response == 2) + { + g_assert_false (res); + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_FAILED); + } + else + g_assert_not_reached (); + + got_info++; + + g_main_context_wakeup (NULL); +} + +static const char * +target_to_string (XdpWallpaperFlags target) +{ + const char *strings[] = { "", "background", "lockscreen", "both" }; + return strings[target & 3]; +} + +void +test_wallpaper_basic (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + g_autofree char *uri = NULL; + XdpWallpaperFlags target = XDP_WALLPAPER_FLAG_BACKGROUND | XDP_WALLPAPER_FLAG_LOCKSCREEN; + + reset_wallpaper_permissions (); + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "access", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + g_free (path); + + g_key_file_set_string (keyfile, "wallpaper", "target", target_to_string (target)); + g_key_file_set_boolean (keyfile, "wallpaper", "preview", FALSE); + + path = g_build_filename (outdir, "wallpaper", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + uri = g_strconcat ("file://", path, NULL); + + got_info = 0; + xdp_portal_set_wallpaper (portal, NULL, uri, target, NULL, wallpaper_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_wallpaper_delay (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + g_autofree char *uri = NULL; + XdpWallpaperFlags target = XDP_WALLPAPER_FLAG_LOCKSCREEN; + + reset_wallpaper_permissions (); + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "access", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + g_free (path); + + g_key_file_set_string (keyfile, "wallpaper", "target", target_to_string (target)); + g_key_file_set_boolean (keyfile, "wallpaper", "preview", FALSE); + + path = g_build_filename (outdir, "wallpaper", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + uri = g_strconcat ("file://", path, NULL); + + got_info = 0; + xdp_portal_set_wallpaper (portal, NULL, uri, target, NULL, wallpaper_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_wallpaper_cancel1 (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + g_autofree char *uri = NULL; + XdpWallpaperFlags target = XDP_WALLPAPER_FLAG_BACKGROUND; + + reset_wallpaper_permissions (); + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 1); + g_key_file_set_integer (keyfile, "result", "response", 1); + + path = g_build_filename (outdir, "access", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + g_free (path); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 2); + g_key_file_set_string (keyfile, "wallpaper", "target", target_to_string (target)); + g_key_file_set_boolean (keyfile, "wallpaper", "preview", FALSE); + + path = g_build_filename (outdir, "wallpaper", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + uri = g_strconcat ("file://", path, NULL); + + got_info = 0; + xdp_portal_set_wallpaper (portal, NULL, uri, target, NULL, wallpaper_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_wallpaper_cancel2 (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + g_autofree char *uri = NULL; + XdpWallpaperFlags target = XDP_WALLPAPER_FLAG_BACKGROUND | XDP_WALLPAPER_FLAG_LOCKSCREEN | XDP_WALLPAPER_FLAG_PREVIEW; + + reset_wallpaper_permissions (); + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "access", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + g_free (path); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 1); + g_key_file_set_integer (keyfile, "result", "response", 1); + g_key_file_set_string (keyfile, "wallpaper", "target", target_to_string (target)); + g_key_file_set_boolean (keyfile, "wallpaper", "preview", TRUE); + + path = g_build_filename (outdir, "wallpaper", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + uri = g_strconcat ("file://", path, NULL); + + got_info = 0; + xdp_portal_set_wallpaper (portal, NULL, uri, target, NULL, wallpaper_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} + +void +test_wallpaper_permission (void) +{ + g_autoptr(XdpPortal) portal = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + g_autofree char *uri = NULL; + XdpWallpaperFlags target = XDP_WALLPAPER_FLAG_BACKGROUND | XDP_WALLPAPER_FLAG_LOCKSCREEN | XDP_WALLPAPER_FLAG_PREVIEW; + + set_wallpaper_permissions ("no"); + + keyfile = g_key_file_new (); + + g_key_file_set_integer (keyfile, "backend", "delay", 0); + g_key_file_set_integer (keyfile, "backend", "response", 0); + g_key_file_set_integer (keyfile, "result", "response", 0); + + path = g_build_filename (outdir, "access", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + g_free (path); + + g_key_file_set_integer (keyfile, "backend", "delay", 200); + g_key_file_set_integer (keyfile, "backend", "response", 1); + g_key_file_set_integer (keyfile, "result", "response", 2); + g_key_file_set_string (keyfile, "wallpaper", "target", target_to_string (target)); + g_key_file_set_boolean (keyfile, "wallpaper", "preview", TRUE); + + path = g_build_filename (outdir, "wallpaper", NULL); + g_key_file_save_to_file (keyfile, path, &error); + g_assert_no_error (error); + + portal = xdp_portal_new (); + + uri = g_strconcat ("file://", path, NULL); + + got_info = 0; + xdp_portal_set_wallpaper (portal, NULL, uri, target, NULL, wallpaper_cb, keyfile); + + while (!got_info) + g_main_context_iteration (NULL, TRUE); +} diff --git a/tests/wallpaper.h b/tests/wallpaper.h new file mode 100644 index 0000000..0c443ac --- /dev/null +++ b/tests/wallpaper.h @@ -0,0 +1,7 @@ +#pragma once + +void test_wallpaper_basic (void); +void test_wallpaper_delay (void); +void test_wallpaper_cancel1 (void); +void test_wallpaper_cancel2 (void); +void test_wallpaper_permission (void); -- cgit v1.2.3 From 211ffec70524273c3d00739a09dbac94eeab637c Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Mon, 7 Aug 2023 12:50:54 +0400 Subject: open-uri: Pass activation token to the FileManager1 interface (cherry picked from commit 69e2faef94f12734706c942ae99248a122fe2f49) Origin: future 1.17.1 Gbp-Pq: Name open-uri-Pass-activation-token-to-the-FileManager1-interf.patch --- src/open-uri.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/open-uri.c b/src/open-uri.c index ef9cf49..9cddc6e 100644 --- a/src/open-uri.c +++ b/src/open-uri.c @@ -741,7 +741,7 @@ handle_open_in_thread_func (GTask *task, FILE_MANAGER_DBUS_PATH, FILE_MANAGER_DBUS_IFACE, FILE_MANAGER_SHOW_ITEMS, - g_variant_new ("(ass)", uris_builder, ""), + g_variant_new ("(ass)", uris_builder, activation_token), NULL, /* ignore returned type */ G_DBUS_CALL_FLAGS_NONE, -1, -- cgit v1.2.3 From eec7c61f6d8ef7ed87457c93298b073e8c2edf36 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Mon, 7 Aug 2023 12:52:32 +0400 Subject: open-uri: Fix a (presumably) copy-paste typo (cherry picked from commit 4434b20487e734b3b42ed0b1d1035a323a37c945) Origin: future 1.17.1 Gbp-Pq: Name open-uri-Fix-a-presumably-copy-paste-typo.patch --- src/open-uri.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/open-uri.c b/src/open-uri.c index 9cddc6e..880399c 100644 --- a/src/open-uri.c +++ b/src/open-uri.c @@ -1074,7 +1074,7 @@ handle_open_directory (XdpDbusOpenURI *object, g_object_set_data_full (G_OBJECT (request), "activation-token", g_strdup (activation_token), g_free); request_export (request, g_dbus_method_invocation_get_connection (invocation)); - xdp_dbus_open_uri_complete_open_file (object, invocation, NULL, request->id); + xdp_dbus_open_uri_complete_open_directory (object, invocation, NULL, request->id); task = g_task_new (object, NULL, NULL, NULL); g_task_set_task_data (task, g_object_ref (request), g_object_unref); -- cgit v1.2.3 From 76f9eb0f395e1232be29a340b878516b85c7e425 Mon Sep 17 00:00:00 2001 From: Robert Ancell Date: Thu, 1 Sep 2022 14:42:02 +1200 Subject: settings: Fix values from read being variants inside variants. The backend returns a variant, and this was mistakenly being put inside another variant when returned to the original caller. Add a new ReadOne method that doesn't have this issue. Fixes https://github.com/flatpak/xdg-desktop-portal/issues/789 (cherry picked from commit c28dbdd990d7abd25a22128c96c6a3c521d2abe0) Origin: future 1.17.1 Gbp-Pq: Name settings-Fix-values-from-read-being-variants-inside-varia.patch --- data/org.freedesktop.portal.Settings.xml | 23 ++++++++++++++++++- src/settings.c | 39 +++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/data/org.freedesktop.portal.Settings.xml b/data/org.freedesktop.portal.Settings.xml index 669997a..7d16cc7 100644 --- a/data/org.freedesktop.portal.Settings.xml +++ b/data/org.freedesktop.portal.Settings.xml @@ -49,7 +49,7 @@ implementation details that are undocumented. If you are a toolkit and want to use this please open an issue. - This documentation describes version 1 of this interface. + This documentation describes version 2 of this interface. --> @@ -73,8 +73,29 @@ @value: The value @key is set to. Reads a single value. Returns an error on any unknown namespace or key. + + Deprecated, use ReadOne instead. The value argument was intended to have + the value inside one layer of variant as it is in ReadOne, for example + `<string "hello">` in GVariant text notation; but it is actually + returned inside two layers of variant, for example + `<<string "hello">>`. --> + + + + + + + + diff --git a/src/settings.c b/src/settings.c index 4a60cdd..4890c03 100644 --- a/src/settings.c +++ b/src/settings.c @@ -129,6 +129,42 @@ settings_handle_read (XdpDbusSettings *object, return G_DBUS_METHOD_INVOCATION_HANDLED; } +static gboolean +settings_handle_read_one (XdpDbusSettings *object, + GDBusMethodInvocation *invocation, + const char *arg_namespace, + const char *arg_key) +{ + int i; + + g_debug ("ReadOne %s %s", arg_namespace, arg_key); + + for (i = 0; i < n_impls; i++) + { + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) impl_value = NULL; + + if (!xdp_dbus_impl_settings_call_read_sync (impls[i], arg_namespace, + arg_key, &impl_value, NULL, &error)) + { + /* A key not being found is expected, continue to our implementation */ + g_debug ("Failed to Read() from Settings implementation: %s", error->message); + } + else + { + g_dbus_method_invocation_return_value (invocation, g_variant_new_tuple (&impl_value, 1)); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + } + + g_debug ("Attempted to read unknown namespace/key pair: %s %s", arg_namespace, arg_key); + g_dbus_method_invocation_return_error_literal (invocation, XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_FOUND, + _("Requested setting not found")); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + static void on_impl_settings_changed (XdpDbusImplSettings *impl, const char *arg_namespace, @@ -145,13 +181,14 @@ static void settings_iface_init (XdpDbusSettingsIface *iface) { iface->handle_read = settings_handle_read; + iface->handle_read_one = settings_handle_read_one; iface->handle_read_all = settings_handle_read_all; } static void settings_init (Settings *settings) { - xdp_dbus_settings_set_version (XDP_DBUS_SETTINGS (settings), 1); + xdp_dbus_settings_set_version (XDP_DBUS_SETTINGS (settings), 2); } static void -- cgit v1.2.3