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() {