From a2c79faafd6cd8b31b60bebfaa9a35bd2859fbbd Mon Sep 17 00:00:00 2001 From: Jan Grulich Date: Fri, 28 Jun 2024 14:33:04 +0000 Subject: [PATCH] Bug 1882079 - Display real path when choosing download directory over portal r=settings-reviewers,Gijs,emilio Use the new API addition to Document portal allowing clients to get real path to the exported document. This allows to still use the same path as provided by the document portal, but display the path as exists on the host side. Differential Revision: https://phabricator.services.mozilla.com/D202717 --- browser/components/preferences/main.js | 95 +++++++++------ xpcom/io/FileDescriptorFile.cpp | 5 + xpcom/io/moz.build | 1 + xpcom/io/nsIFile.idl | 11 ++ xpcom/io/nsLocalFileUnix.cpp | 161 ++++++++++++++++++++++++- xpcom/io/nsLocalFileWin.cpp | 5 + 6 files changed, 243 insertions(+), 35 deletions(-) diff --git a/browser/components/preferences/main.js b/browser/components/preferences/main.js index 2db0afbf3835..fec5aa685ea7 100644 --- a/browser/components/preferences/main.js +++ b/browser/components/preferences/main.js @@ -3606,46 +3606,73 @@ var gMainPane = { ]); } } - if (firefoxLocalizedName) { - let folderDisplayName, leafName; - // Either/both of these can throw, so check for failures in both cases - // so we don't just break display of the download pref: - try { - folderDisplayName = file.displayName; - } catch (ex) { - /* ignored */ - } - try { - leafName = file.leafName; - } catch (ex) { - /* ignored */ - } - // If we found a localized name that's different from the leaf name, - // use that: - if (folderDisplayName && folderDisplayName != leafName) { - return { file, folderDisplayName }; - } + if (file) { + let displayName = file.path; - // Otherwise, check if we've got a localized name ourselves. - if (firefoxLocalizedName) { - // You can't move the system download or desktop dir on macOS, - // so if those are in use just display them. On other platforms - // only do so if the folder matches the localized name. - if ( - AppConstants.platform == "macosx" || - leafName == firefoxLocalizedName - ) { - return { file, folderDisplayName: firefoxLocalizedName }; + // Attempt to translate path to the path as exists on the host + // in case the provided path comes from the document portal + if (AppConstants.platform == "linux") { + try { + displayName = await file.hostPath(); + } catch (error) { + /* ignored */ + } + + if (displayName) { + if (displayName == downloadsDir.path) { + firefoxLocalizedName = await document.l10n.formatValues([ + { id: "downloads-folder-name" }, + ]); + } else if (displayName == desktopDir.path) { + firefoxLocalizedName = await document.l10n.formatValues([ + { id: "desktop-folder-name" }, + ]); + } } } - } - // If we get here, attempts to use a "pretty" name failed. Just display - // the full path: - if (file) { + + if (firefoxLocalizedName) { + let folderDisplayName, leafName; + // Either/both of these can throw, so check for failures in both cases + // so we don't just break display of the download pref: + try { + folderDisplayName = file.displayName; + } catch (ex) { + /* ignored */ + } + try { + leafName = file.leafName; + } catch (ex) { + /* ignored */ + } + + // If we found a localized name that's different from the leaf name, + // use that: + if (folderDisplayName && folderDisplayName != leafName) { + return { file, folderDisplayName }; + } + + // Otherwise, check if we've got a localized name ourselves. + if (firefoxLocalizedName) { + // You can't move the system download or desktop dir on macOS, + // so if those are in use just display them. On other platforms + // only do so if the folder matches the localized name. + if ( + AppConstants.platform == "macosx" || + leafName == firefoxLocalizedName + ) { + return { file, folderDisplayName: firefoxLocalizedName }; + } + } + } + + // If we get here, attempts to use a "pretty" name failed. Just display + // the full path: // Force the left-to-right direction when displaying a custom path. - return { file, folderDisplayName: `\u2066${file.path}\u2069` }; + return { file, folderDisplayName: `\u2066${displayName}\u2069` }; } + // Don't even have a file - fall back to desktop directory for the // use of the icon, and an empty label: file = desktopDir; diff --git a/xpcom/io/FileDescriptorFile.cpp b/xpcom/io/FileDescriptorFile.cpp index 6398a89760fe..732c6547fce4 100644 --- a/xpcom/io/FileDescriptorFile.cpp +++ b/xpcom/io/FileDescriptorFile.cpp @@ -164,6 +164,11 @@ FileDescriptorFile::SetNativeLeafName(const nsACString& aLeafName) { return NS_ERROR_NOT_IMPLEMENTED; } +NS_IMETHODIMP +FileDescriptorFile::HostPath(JSContext* aCx, dom::Promise** aPromise) { + return NS_ERROR_NOT_IMPLEMENTED; +} + nsresult FileDescriptorFile::InitWithPath(const nsAString& aPath) { return NS_ERROR_NOT_IMPLEMENTED; } diff --git a/xpcom/io/moz.build b/xpcom/io/moz.build index 91dca0cdd185..f137529b5b0d 100644 --- a/xpcom/io/moz.build +++ b/xpcom/io/moz.build @@ -60,6 +60,7 @@ else: UNIFIED_SOURCES += [ "nsLocalFileUnix.cpp", ] + CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"] XPIDL_MODULE = "xpcom_io" diff --git a/xpcom/io/nsIFile.idl b/xpcom/io/nsIFile.idl index 89c2155e945c..46ae360de60d 100644 --- a/xpcom/io/nsIFile.idl +++ b/xpcom/io/nsIFile.idl @@ -128,6 +128,17 @@ interface nsIFile : nsISupports */ readonly attribute AString displayName; + /** + * Linux/Flatpak specific + * Returns path as exists on the host. Translates path provided by the document + * portal to the path it represents on the host. + * @returns {Promise} that resolves with translated path + * if applicable or path as it is. Rejects when Firefox runs as Flatpak and we + * failed to translate the path. + */ + [implicit_jscontext] + Promise hostPath(); + /** * copyTo[Native] * diff --git a/xpcom/io/nsLocalFileUnix.cpp b/xpcom/io/nsLocalFileUnix.cpp index ae501f404154..8719777559b0 100644 --- a/xpcom/io/nsLocalFileUnix.cpp +++ b/xpcom/io/nsLocalFileUnix.cpp @@ -16,6 +16,7 @@ #include "mozilla/DebugOnly.h" #include "mozilla/Sprintf.h" #include "mozilla/FilePreferences.h" +#include "mozilla/dom/Promise.h" #include "prtime.h" #include @@ -51,6 +52,11 @@ #ifdef MOZ_WIDGET_GTK # include "nsIGIOService.h" +# ifdef MOZ_ENABLE_DBUS +# include "mozilla/widget/AsyncDBus.h" +# include "mozilla/WidgetUtilsGtk.h" +# include +# endif #endif #ifdef MOZ_WIDGET_COCOA @@ -117,6 +123,19 @@ using namespace mozilla; return NS_ERROR_FILE_ACCESS_DENIED; \ } while (0) +// Prefix for files exported through document portal when we are +// in a sandboxed environment (Flatpak). +static const nsCString& GetDocumentStorePath() { + static const nsDependentCString sDocumentStorePath = [] { + nsCString storePath = nsPrintfCString("/run/user/%d/doc/", getuid()); + // Intentionally put into a ToNewCString copy, rather than just making a + // static nsCString to avoid leakchecking errors, since we really want to + // leak this string. + return nsDependentCString(ToNewCString(storePath), storePath.Length()); + }(); + return sDocumentStorePath; +} + static PRTime TimespecToMillis(const struct timespec& aTimeSpec) { return PRTime(aTimeSpec.tv_sec) * PR_MSEC_PER_SEC + PRTime(aTimeSpec.tv_nsec) / PR_NSEC_PER_MSEC; @@ -223,7 +242,7 @@ nsDirEnumeratorUnix::GetNextEntry() { // keep going past "." and ".." } while (mEntry->d_name[0] == '.' && - (mEntry->d_name[1] == '\0' || // .\0 + (mEntry->d_name[1] == '\0' || // .\0 (mEntry->d_name[1] == '.' && mEntry->d_name[2] == '\0'))); // ..\0 return NS_OK; } @@ -673,6 +692,146 @@ nsLocalFile::GetDisplayName(nsAString& aLeafName) { return GetLeafName(aLeafName); } +NS_IMETHODIMP +nsLocalFile::HostPath(JSContext* aCx, dom::Promise** aPromise) { + MOZ_ASSERT(aCx); + MOZ_ASSERT(aPromise); + + nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx); + if (NS_WARN_IF(!globalObject)) { + return NS_ERROR_FAILURE; + } + + ErrorResult result; + RefPtr retPromise = dom::Promise::Create(globalObject, result); + if (NS_WARN_IF(result.Failed())) { + return result.StealNSResult(); + } + +#if defined(MOZ_ENABLE_DBUS) && defined(MOZ_WIDGET_GTK) + if (!widget::IsRunningUnderFlatpak() || + !StringBeginsWith(mPath, GetDocumentStorePath())) { + retPromise->MaybeResolve(mPath); + retPromise.forget(aPromise); + return NS_OK; + } + + nsCString docId = [this] { + auto subPath = Substring(mPath, GetDocumentStorePath().Length()); + if (auto idx = subPath.Find("/"); idx > 0) { + subPath.Truncate(idx); + } + return nsCString(subPath); + }(); + + const char kServiceName[] = "org.freedesktop.portal.Documents"; + const char kDBusPath[] = "/org/freedesktop/portal/documents"; + const char kInterfaceName[] = "org.freedesktop.portal.Documents"; + + widget::CreateDBusProxyForBus(G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_NONE, + /* aInterfaceInfo = */ nullptr, kServiceName, + kDBusPath, kInterfaceName) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [this, self = RefPtr(this), docId, + retPromise](RefPtr&& aProxy) { + RefPtr version = dont_AddRef( + g_dbus_proxy_get_cached_property(aProxy, "version")); + if (!version || + !g_variant_is_of_type(version, G_VARIANT_TYPE_UINT32)) { + g_printerr( + "nsIFile: failed to get host path for %s\n: Invalid value.", + mPath.get()); + retPromise->MaybeReject(NS_ERROR_FAILURE); + return; + } + + if (g_variant_get_uint32(version) < 5) { + g_printerr( + "nsIFile: failed to get host path for %s\n: Document " + "portal in version 5 is required.", + mPath.get()); + retPromise->MaybeReject(NS_ERROR_NOT_AVAILABLE); + return; + } + + GVariantBuilder builder; + g_variant_builder_init(&builder, G_VARIANT_TYPE("(as)")); + g_variant_builder_open(&builder, G_VARIANT_TYPE("as")); + g_variant_builder_add(&builder, "s", docId.get()); + g_variant_builder_close(&builder); + + RefPtr args = dont_AddRef( + g_variant_ref_sink(g_variant_builder_end(&builder))); + + if (!args) { + g_printerr( + "nsIFile: failed to get host path for %s\n: " + "Invalid value.", + mPath.get()); + retPromise->MaybeReject(NS_ERROR_FAILURE); + return; + } + + widget::DBusProxyCall(aProxy, "GetHostPaths", args, + G_DBUS_CALL_FLAGS_NONE, -1, + /* cancellable */ nullptr) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [this, self = RefPtr(this), docId, + retPromise](RefPtr&& aResult) { + RefPtr result = dont_AddRef( + g_variant_get_child_value(aResult.get(), 0)); + if (!g_variant_is_of_type(result, + G_VARIANT_TYPE("a{say}"))) { + g_printerr( + "nsIFile: failed to get host path for %s\n: " + "Invalid value.", + mPath.get()); + retPromise->MaybeReject(NS_ERROR_FAILURE); + return; + } + + const gchar* key = nullptr; + const gchar* path = nullptr; + GVariantIter* iter = g_variant_iter_new(result); + + while ( + g_variant_iter_loop(iter, "{&s^&ay}", &key, &path)) { + if (g_strcmp0(key, docId.get()) == 0) { + retPromise->MaybeResolve(nsDependentCString(path)); + g_variant_iter_free(iter); + return; + } + } + + g_variant_iter_free(iter); + g_printerr( + "nsIFile: failed to get host path for %s\n: " + "Invalid value.", + mPath.get()); + retPromise->MaybeReject(NS_ERROR_FAILURE); + }, + [this, self = RefPtr(this), + retPromise](GUniquePtr&& aError) { + g_printerr( + "nsIFile: failed to get host path for %s\n: %s.", + mPath.get(), aError->message); + retPromise->MaybeReject(NS_ERROR_FAILURE); + }); + }, + [this, self = RefPtr(this), retPromise](GUniquePtr&& aError) { + g_printerr("nsIFile: failed to get host path for %s\n: %s.", + mPath.get(), aError->message); + retPromise->MaybeReject(NS_ERROR_NOT_AVAILABLE); + }); +#else + retPromise->MaybeResolve(mPath); +#endif + retPromise.forget(aPromise); + return NS_OK; +} + nsCString nsLocalFile::NativePath() { return mPath; } nsresult nsIFile::GetNativePath(nsACString& aResult) { diff --git a/xpcom/io/nsLocalFileWin.cpp b/xpcom/io/nsLocalFileWin.cpp index b11b4da4e7ac..8b910f978f5d 100644 --- a/xpcom/io/nsLocalFileWin.cpp +++ b/xpcom/io/nsLocalFileWin.cpp @@ -3521,6 +3521,11 @@ nsLocalFile::SetNativeLeafName(const nsACString& aLeafName) { return rv; } +NS_IMETHODIMP +nsLocalFile::HostPath(JSContext* aCx, dom::Promise** aPromise) { + return NS_ERROR_NOT_IMPLEMENTED; +} + nsString nsLocalFile::NativePath() { return mWorkingPath; } nsCString nsIFile::HumanReadablePath() {