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
This commit is contained in:
Jan Grulich 2024-06-28 19:31:13 +00:00
parent fb2d496b92
commit d2084644e7
6 changed files with 245 additions and 35 deletions

View File

@ -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;

View File

@ -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;
}

View File

@ -60,6 +60,7 @@ else:
UNIFIED_SOURCES += [
"nsLocalFileUnix.cpp",
]
CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
XPIDL_MODULE = "xpcom_io"

View File

@ -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<nsCString, nsresult>} 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]
*

View File

@ -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 <sys/select.h>
@ -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 <map>
# endif
#endif
#ifdef MOZ_WIDGET_COCOA
@ -117,6 +123,21 @@ using namespace mozilla;
return NS_ERROR_FILE_ACCESS_DENIED; \
} while (0)
#if defined(MOZ_ENABLE_DBUS) && defined(MOZ_WIDGET_GTK)
// 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;
}
#endif
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 +244,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 +694,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<dom::Promise> 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<GDBusProxy>&& aProxy) {
RefPtr<GVariant> 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<GVariant> 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<GVariant>&& aResult) {
RefPtr<GVariant> 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<GError>&& 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<GError>&& 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) {

View File

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