#include "shell/common/platform_util.h"
#include <fcntl.h>
#include <memory>
#include <optional>
#include <queue>
#include <string>
#include <vector>
#include <gdk/gdk.h>
#include "base/containers/flat_set.h"
#include "base/containers/unique_ptr_adapters.h"
#include "base/environment.h"
#include "base/files/file_util.h"
#include "base/files/scoped_file.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/nix/xdg_util.h"
#include "base/no_destructor.h"
#include "base/posix/eintr_wrapper.h"
#include "base/process/kill.h"
#include "base/process/launch.h"
#include "base/run_loop.h"
#include "base/strings/escape.h"
#include "base/task/thread_pool.h"
#include "base/threading/thread_restrictions.h"
#include "base/types/expected.h"
#include "components/dbus/thread_linux/dbus_thread_linux.h"
#include "components/dbus/utils/call_method.h"
#include "components/dbus/utils/check_for_service_and_start.h"
#include "components/dbus/xdg/request.h"
#include "content/public/browser/browser_thread.h"
#include "dbus/bus.h"
#include "dbus/message.h"
#include "dbus/object_proxy.h"
#include "ui/gtk/gtk_compat.h"
#include "electron/electron_gtk_stubs.h"
#include "shell/common/platform_util_internal.h"
#include "url/gurl.h"
#define ELECTRON_TRASH "ELECTRON_TRASH"
namespace platform_util {
void OpenFolder(const base::FilePath& full_path);
}
namespace {
const char kFreedesktopFileManagerName[] = "org.freedesktop.FileManager1";
const char kFreedesktopFileManagerPath[] = "/org/freedesktop/FileManager1";
const char kMethodShowItems[] = "ShowItems";
const char kFreedesktopPortalName[] = "org.freedesktop.portal.Desktop";
const char kFreedesktopPortalPath[] = "/org/freedesktop/portal/desktop";
const char kFreedesktopPortalOpenURI[] = "org.freedesktop.portal.OpenURI";
const char kMethodOpenDirectory[] = "OpenDirectory";
const char kActivationTokenKey[] = "activation_token";
class ShowItemHelper {
public:
static ShowItemHelper& GetInstance() {
static base::NoDestructor<ShowItemHelper> instance;
return *instance;
}
ShowItemHelper() = default;
ShowItemHelper(const ShowItemHelper&) = delete;
ShowItemHelper& operator=(const ShowItemHelper&) = delete;
void ShowItemInFolder(const base::FilePath& full_path) {
if (!bus_) {
bus_ = dbus_thread_linux::GetSharedSessionBus();
}
if (api_type_.has_value()) {
ShowItemInFolderOnApiTypeSet(full_path);
return;
}
bool api_availability_check_in_progress = !pending_requests_.empty();
pending_requests_.push(full_path);
if (!api_availability_check_in_progress) {
dbus_utils::CheckForServiceAndStart(
bus_.get(), kFreedesktopPortalName,
base::BindOnce(&ShowItemHelper::CheckPortalRunningResponse,
base::Unretained(this)));
}
}
private:
enum class ApiType { kNone, kPortal, kFileManager };
void ShowItemInFolderOnApiTypeSet(const base::FilePath& full_path) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
CHECK(api_type_.has_value());
switch (*api_type_) {
case ApiType::kPortal:
ShowItemUsingPortal(full_path);
break;
case ApiType::kFileManager:
ShowItemUsingFileManager(full_path);
break;
case ApiType::kNone:
OpenParentFolderFallback(full_path);
break;
}
}
void ProcessPendingRequests() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!bus_) {
return;
}
CHECK(!pending_requests_.empty());
while (!pending_requests_.empty()) {
ShowItemInFolderOnApiTypeSet(pending_requests_.front());
pending_requests_.pop();
}
}
void CheckPortalRunningResponse(std::optional<bool> is_running) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (is_running.value_or(false)) {
api_type_ = ApiType::kPortal;
ProcessPendingRequests();
} else {
dbus_utils::CheckForServiceAndStart(
bus_.get(), kFreedesktopFileManagerName,
base::BindOnce(&ShowItemHelper::CheckFileManagerRunningResponse,
base::Unretained(this)));
}
}
void CheckFileManagerRunningResponse(std::optional<bool> is_running) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (is_running.value_or(false)) {
api_type_ = ApiType::kFileManager;
} else {
api_type_ = ApiType::kNone;
}
ProcessPendingRequests();
}
void ShowItemUsingPortal(const base::FilePath& full_path) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
CHECK(api_type_.has_value());
CHECK_EQ(*api_type_, ApiType::kPortal);
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock()},
base::BindOnce(
[](const base::FilePath& full_path) {
base::ScopedFD fd(HANDLE_EINTR(
open(full_path.value().c_str(), O_RDONLY | O_CLOEXEC)));
return fd;
},
full_path),
base::BindOnce(&ShowItemHelper::ShowItemUsingPortalFdOpened,
base::Unretained(this), full_path));
}
void ShowItemUsingPortalFdOpened(const base::FilePath& full_path,
base::ScopedFD fd) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!bus_) {
return;
}
if (!fd.is_valid()) {
OpenParentFolderFallback(full_path);
return;
}
base::nix::CreateXdgActivationToken(base::BindOnce(
&ShowItemHelper::ShowItemUsingPortalWithToken,
base::Unretained(this), full_path, std::move(fd)));
}
void ShowItemUsingPortalWithToken(const base::FilePath& full_path,
base::ScopedFD fd,
std::string activation_token) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!bus_) {
return;
}
if (!portal_object_proxy_) {
portal_object_proxy_ = bus_->GetObjectProxy(
kFreedesktopPortalName, dbus::ObjectPath(kFreedesktopPortalPath));
}
dbus_xdg::Dictionary options;
options[kActivationTokenKey] =
dbus_utils::Variant::Wrap<"s">(activation_token);
auto request = std::make_unique<dbus_xdg::Request>(
bus_, portal_object_proxy_, kFreedesktopPortalOpenURI,
kMethodOpenDirectory, std::move(options), std::string(), std::move(fd));
request->SetCallback(
base::BindOnce(&ShowItemHelper::ShowItemUsingPortalResponse,
base::Unretained(this), request.get(), full_path));
portal_requests_.insert(std::move(request));
}
void ShowItemUsingPortalResponse(
dbus_xdg::Request* request,
const base::FilePath& full_path,
base::expected<dbus_xdg::Dictionary, dbus_xdg::ResponseError> results) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!results.has_value() &&
results.error() != dbus_xdg::ResponseError::kRequestCancelledByUser) {
OpenParentFolderFallback(full_path);
}
CHECK_EQ(portal_requests_.erase(request), 1u);
}
void ShowItemUsingFileManager(const base::FilePath& full_path) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!bus_) {
return;
}
CHECK(api_type_.has_value());
CHECK_EQ(*api_type_, ApiType::kFileManager);
if (!file_manager_object_proxy_) {
file_manager_object_proxy_ =
bus_->GetObjectProxy(kFreedesktopFileManagerName,
dbus::ObjectPath(kFreedesktopFileManagerPath));
}
std::vector<std::string> file_to_highlight{
"file://" + base::EscapePath(full_path.value())};
dbus_utils::CallMethod<"ass", "">(
file_manager_object_proxy_, kFreedesktopFileManagerName,
kMethodShowItems,
base::BindOnce(&ShowItemHelper::ShowItemUsingFileManagerResponse,
base::Unretained(this), full_path),
file_to_highlight, "");
}
void ShowItemUsingFileManagerResponse(
const base::FilePath& full_path,
dbus_utils::CallMethodResultSig<""> response) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!response.has_value()) {
OpenParentFolderFallback(full_path);
}
}
void OpenParentFolderFallback(const base::FilePath& full_path) {
platform_util::OpenFolder(full_path.DirName());
}
scoped_refptr<dbus::Bus> bus_;
std::optional<ApiType> api_type_;
raw_ptr<dbus::ObjectProxy> portal_object_proxy_ = nullptr;
raw_ptr<dbus::ObjectProxy> file_manager_object_proxy_ = nullptr;
base::flat_set<std::unique_ptr<dbus_xdg::Request>, base::UniquePtrComparator>
portal_requests_;
std::queue<base::FilePath> pending_requests_;
};
std::string GetErrorDescription(int error_code) {
switch (error_code) {
case 1:
return "Error in command line syntax";
case 2:
return "The item does not exist";
case 3:
return "A required tool could not be found";
case 4:
return "The action failed";
default:
return "";
}
}
bool XDGUtil(const std::vector<std::string>& argv,
const base::FilePath& working_directory,
const bool wait_for_exit,
const bool focus_launched_process,
platform_util::OpenCallback callback) {
base::LaunchOptions options;
if (focus_launched_process) {
base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed);
base::RepeatingClosure quit_loop = run_loop.QuitClosure();
base::nix::CreateLaunchOptionsWithXdgActivation(base::BindOnce(
[](base::RepeatingClosure quit_loop, base::LaunchOptions* options_out,
base::LaunchOptions options) {
*options_out = options;
std::move(quit_loop).Run();
},
std::move(quit_loop), &options));
run_loop.Run();
}
options.current_directory = working_directory;
options.allow_new_privs = true;
options.environment["MM_NOTTTY"] = "1";
base::Process process = base::LaunchProcess(argv, options);
if (!process.IsValid()) {
if (!callback.is_null())
std::move(callback).Run("Failed to launch " + argv[0]);
return false;
}
if (wait_for_exit) {
base::ScopedAllowBaseSyncPrimitivesForTesting
allow_sync;
int exit_code = -1;
bool success = process.WaitForExit(&exit_code);
if (!callback.is_null())
std::move(callback).Run(GetErrorDescription(exit_code));
return success ? (exit_code == 0) : false;
}
base::EnsureProcessGetsReaped(std::move(process));
if (!callback.is_null())
std::move(callback).Run("");
return true;
}
bool XDGOpen(const base::FilePath& working_directory,
const std::string& path,
const bool wait_for_exit,
platform_util::OpenCallback callback) {
return XDGUtil({"xdg-open", path}, working_directory, wait_for_exit,
true, std::move(callback));
}
bool XDGEmail(const std::string& email, const bool wait_for_exit) {
return XDGUtil({"xdg-email", email}, base::FilePath(), wait_for_exit,
true,
platform_util::OpenCallback());
}
}
namespace platform_util {
void ShowItemInFolder(const base::FilePath& full_path) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
ShowItemHelper::GetInstance().ShowItemInFolder(full_path);
}
void OpenPath(const base::FilePath& full_path, OpenCallback callback) {
XDGOpen(full_path.DirName(), full_path.value(), false, std::move(callback));
}
void OpenFolder(const base::FilePath& full_path) {
if (!base::DirectoryExists(full_path))
return;
XDGOpen(full_path.DirName(), ".", false, platform_util::OpenCallback());
}
void OpenExternal(const GURL& url,
const OpenExternalOptions& options,
OpenCallback callback) {
if (url.SchemeIs("mailto")) {
bool success = XDGEmail(url.spec(), false);
std::move(callback).Run(success ? "" : "Failed to open path");
} else {
bool success = XDGOpen(base::FilePath(), url.spec(), false,
platform_util::OpenCallback());
std::move(callback).Run(success ? "" : "Failed to open path");
}
}
bool MoveItemToTrash(const base::FilePath& full_path, bool delete_on_fail) {
auto env = base::Environment::Create();
std::string trash = env->GetVar(ELECTRON_TRASH).value_or("");
if (trash.empty()) {
switch (base::nix::GetDesktopEnvironment(env.get())) {
case base::nix::DESKTOP_ENVIRONMENT_KDE4:
case base::nix::DESKTOP_ENVIRONMENT_KDE5:
trash = "kioclient5";
break;
case base::nix::DESKTOP_ENVIRONMENT_KDE3:
case base::nix::DESKTOP_ENVIRONMENT_KDE6:
trash = "kioclient";
break;
case base::nix::DESKTOP_ENVIRONMENT_OTHER:
case base::nix::DESKTOP_ENVIRONMENT_CINNAMON:
case base::nix::DESKTOP_ENVIRONMENT_DEEPIN:
case base::nix::DESKTOP_ENVIRONMENT_GNOME:
case base::nix::DESKTOP_ENVIRONMENT_PANTHEON:
case base::nix::DESKTOP_ENVIRONMENT_UKUI:
case base::nix::DESKTOP_ENVIRONMENT_UNITY:
case base::nix::DESKTOP_ENVIRONMENT_XFCE:
case base::nix::DESKTOP_ENVIRONMENT_LXQT:
case base::nix::DESKTOP_ENVIRONMENT_COSMIC:
break;
}
}
std::vector<std::string> argv;
const auto& filename = full_path.value();
if (trash == "kioclient5" || trash == "kioclient") {
argv = {trash, "move", filename, "trash:/"};
} else if (trash == "trash-cli") {
argv = {"trash-put", filename};
} else if (trash == "gvfs-trash") {
argv = {"gvfs-trash", filename};
} else {
argv = {"gio", "trash", filename};
}
return XDGUtil(argv, base::FilePath(), true, false,
platform_util::OpenCallback());
}
namespace internal {
bool PlatformTrashItem(const base::FilePath& full_path, std::string* error) {
if (!MoveItemToTrash(full_path, false)) {
*error = "Failed to move item to trash";
return false;
}
return true;
}
}
void Beep() {
if (!electron::IsElectron_gdkInitialized()) {
electron::InitializeElectron_gdk(gtk::GetLibGdk());
CHECK(electron::IsElectron_gdkInitialized())
<< "Failed to initialize libgdk";
}
auto* display = gdk_display_get_default();
if (!display)
return;
gdk_display_beep(display);
}
std::optional<std::string> GetDesktopName() {
return base::Environment::Create()->GetVar("CHROME_DESKTOP");
}
std::optional<std::string> GetXdgAppId() {
auto name = GetDesktopName();
if (!name)
return {};
if (std::string_view suffix = ".desktop"; name->ends_with(suffix))
name->resize(std::size(*name) - std::size(suffix));
return *name;
}
}