Bug 1411589 - Implement printing support for the flatpak portal, r?stransky draft
authorJan Horak <jhorak@redhat.com>
Thu, 05 Apr 2018 16:44:35 +0200
changeset 783054 ee760f45764d6ce9c683a572f94a23beb1111c8a
parent 783053 36ed06cf9706e61dd2e58233fc72e3a42a3858f0
child 783055 8ada53df8bfed6b44166c7f1bb11ab7e97c0fe43
push id106586
push userbmo:jhorak@redhat.com
push dateMon, 16 Apr 2018 14:07:20 +0000
reviewersstransky
bugs1411589
milestone61.0a1
Bug 1411589 - Implement printing support for the flatpak portal, r?stransky In the flatpak environment the applications do not have access to the printers. They need to use printing portal implemented by DBUS interface. The patch implements support for printing portal by introducing nsFlatpakPrintPortal class. 1. it request print portal to show the print dialog 2. waits until print dialog is finished 3. setup observer for 'print-to-file-finished' topic 4. pass file descriptor of the printed file to the portal when the observer is notified MozReview-Commit-ID: 3nZtYx7KzK6
widget/gtk/nsPrintDialogGTK.cpp
--- a/widget/gtk/nsPrintDialogGTK.cpp
+++ b/widget/gtk/nsPrintDialogGTK.cpp
@@ -19,17 +19,30 @@
 #include "nsIFile.h"
 #include "nsIStringBundle.h"
 #include "nsIPrintSettingsService.h"
 #include "nsIDOMWindow.h"
 #include "nsPIDOMWindow.h"
 #include "nsIBaseWindow.h"
 #include "nsIDocShellTreeItem.h"
 #include "nsIDocShell.h"
+#include "nsIGIOService.h"
 #include "WidgetUtils.h"
+#include "nsIObserverService.h"
+
+// for gdk_x11_window_get_xid
+#include <gdk/gdkx.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <gio/gunixfdlist.h>
+
+// for dlsym
+#include <dlfcn.h>
+#include "MainThreadUtils.h"
 
 using namespace mozilla;
 using namespace mozilla::widget;
 
 static const char header_footer_tags[][4] =  {"", "&T", "&U", "&D", "&P", "&PT"};
 
 #define CUSTOM_VALUE_INDEX gint(ArrayLength(header_footer_tags))
 
@@ -508,24 +521,525 @@ nsPrintDialogServiceGTK::~nsPrintDialogS
 }
 
 NS_IMETHODIMP
 nsPrintDialogServiceGTK::Init()
 {
   return NS_OK;
 }
 
+// Used to obtain window handle. The portal use this handle
+// to ensure that print dialog is modal.
+typedef void (*WindowHandleExported) (GtkWindow  *window,
+                                      const char *handle,
+                                      gpointer    user_data);
+
+typedef void (*GtkWindowHandleExported) (GtkWindow  *window,
+                                         const char *handle,
+                                         gpointer    user_data);
+#ifdef MOZ_WAYLAND
+typedef struct {
+    GtkWindow *window;
+    WindowHandleExported callback;
+    gpointer user_data;
+} WaylandWindowHandleExportedData;
+
+static void
+wayland_window_handle_exported (GdkWindow  *window,
+                                const char *wayland_handle_str,
+                                gpointer    user_data)
+{
+    WaylandWindowHandleExportedData *data =
+        static_cast<WaylandWindowHandleExportedData*>(user_data);
+    char *handle_str;
+
+    handle_str = g_strdup_printf ("wayland:%s", wayland_handle_str);
+    data->callback (data->window, handle_str, data->user_data);
+    g_free (handle_str);
+}
+#endif
+
+// Get window handle for the portal, taken from gtk/gtkwindow.c
+// (currently not exported)
+static gboolean
+window_export_handle(GtkWindow               *window,
+                     GtkWindowHandleExported  callback,
+                     gpointer                 user_data)
+{
+  if (GDK_IS_X11_DISPLAY(gtk_widget_get_display(GTK_WIDGET(window))))
+    {
+      GdkWindow *gdk_window = gtk_widget_get_window(GTK_WIDGET(window));
+      char *handle_str;
+      guint32 xid = (guint32) gdk_x11_window_get_xid(gdk_window);
+
+      handle_str = g_strdup_printf("x11:%x", xid);
+      callback(window, handle_str, user_data);
+      g_free(handle_str);
+      return true;
+    }
+#ifdef MOZ_WAYLAND
+  else
+    {
+      GdkWindow *gdk_window = gtk_widget_get_window(GTK_WIDGET(window));
+      WaylandWindowHandleExportedData *data;
+
+      data = g_new0(WaylandWindowHandleExportedData, 1);
+      data->window = window;
+      data->callback = callback;
+      data->user_data = user_data;
+
+      static auto s_gdk_wayland_window_export_handle =
+        reinterpret_cast<gboolean (*)(GdkWindow*, GdkWaylandWindowExported,
+                                      gpointer, GDestroyNotify)>
+        (dlsym(RTLD_DEFAULT, "gdk_wayland_window_export_handle"));
+      if (!s_gdk_wayland_window_export_handle ||
+          !s_gdk_wayland_window_export_handle(gdk_window,
+                                              wayland_window_handle_exported,
+                                              data, g_free)) {
+          g_free (data);
+          return false;
+        } else  {
+          return true;
+        }
+    }
+#endif
+
+  g_warning("Couldn't export handle, unsupported windowing system");
+
+  return false;
+}
+/**
+ * Communication class with the GTK print portal handler
+ *
+ * To print document from flatpak we need to use print portal because
+ * printers are not directly accessible in the sandboxed environment.
+ *
+ * At first we request portal to show the print dialog to let user choose
+ * printer settings. We use DBUS interface for that (PreparePrint method).
+ *
+ * Next we force application to print to temporary file and after the writing
+ * to the file is finished we pass its file descriptor to the portal.
+ * Portal will pass duplicate of the file descriptor to the printer which
+ * user selected before (by DBUS Print method).
+ *
+ * Since DBUS communication is done async while nsPrintDialogServiceGTK::Show
+ * is expecting sync execution, we need to create a new GMainLoop during the
+ * print portal dialog is running. The loop is stopped after the dialog
+ * is closed.
+ */
+class nsFlatpakPrintPortal: public nsIObserver
+{
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSIOBSERVER
+  public:
+    explicit nsFlatpakPrintPortal(nsPrintSettingsGTK* aPrintSettings);
+    nsresult PreparePrintRequest(GtkWindow* aWindow);
+    static void OnWindowExportHandleDone(GtkWindow *aWindow,
+                                         const char* aWindowHandleStr,
+                                         gpointer aUserData);
+    void PreparePrint(GtkWindow* aWindow, const char* aWindowHandleStr);
+    static void OnPreparePrintResponse(GDBusConnection *connection,
+                                       const char      *sender_name,
+                                       const char      *object_path,
+                                       const char      *interface_name,
+                                       const char      *signal_name,
+                                       GVariant        *parameters,
+                                       gpointer         data);
+    GtkPrintOperationResult GetResult();
+  private:
+    virtual ~nsFlatpakPrintPortal();
+    void FinishPrintDialog(GVariant* parameters);
+    nsCOMPtr<nsPrintSettingsGTK> mPrintAndPageSettings;
+    GDBusProxy* mProxy;
+    guint32 mToken;
+    GMainLoop* mLoop;
+    GtkPrintOperationResult mResult;
+    guint mResponseSignalId;
+    GtkWindow* mParentWindow;
+};
+
+NS_IMPL_ISUPPORTS(nsFlatpakPrintPortal, nsIObserver)
+
+nsFlatpakPrintPortal::nsFlatpakPrintPortal(nsPrintSettingsGTK* aPrintSettings):
+  mPrintAndPageSettings(aPrintSettings),
+  mProxy(nullptr),
+  mLoop(nullptr),
+  mParentWindow(nullptr)
+{
+}
+
+/**
+ * Creates GDBusProxy, query for window handle and create a new GMainLoop.
+ *
+ * The GMainLoop is to be run from GetResult() and be quitted during
+ * FinishPrintDialog.
+ *
+ * @param aWindow toplevel application window which is used as parent of print
+ *                dialog
+ */
+nsresult
+nsFlatpakPrintPortal::PreparePrintRequest(GtkWindow* aWindow)
+{
+  NS_PRECONDITION(aWindow, "aWindow must not be null");
+  NS_PRECONDITION(mPrintAndPageSettings, "mPrintAndPageSettings must not be null");
+
+  GError* error = nullptr;
+  mProxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION,
+      G_DBUS_PROXY_FLAGS_NONE,
+      nullptr,
+      "org.freedesktop.portal.Desktop",
+      "/org/freedesktop/portal/desktop",
+      "org.freedesktop.portal.Print",
+      nullptr,
+      &error);
+  if (mProxy == nullptr) {
+    NS_WARNING(nsPrintfCString("Unable to create dbus proxy: %s", error->message).get());
+    g_error_free(error);
+    return NS_ERROR_FAILURE;
+  }
+
+  // The window handler is returned async, we will continue by PreparePrint method
+  // when it is returned.
+  if (!window_export_handle(aWindow,
+        &nsFlatpakPrintPortal::OnWindowExportHandleDone, this)) {
+    NS_WARNING("Unable to get window handle for creating modal print dialog.");
+    return NS_ERROR_FAILURE;
+  }
+
+  mLoop = g_main_loop_new (NULL, FALSE);
+  return NS_OK;
+}
+
+void
+nsFlatpakPrintPortal::OnWindowExportHandleDone(GtkWindow* aWindow,
+                                               const char* aWindowHandleStr,
+                                               gpointer aUserData)
+{
+  nsFlatpakPrintPortal* printPortal = static_cast<nsFlatpakPrintPortal*>(aUserData);
+  printPortal->PreparePrint(aWindow, aWindowHandleStr);
+}
+
+/**
+ * Ask print portal to show the print dialog.
+ *
+ * Print and page settings and window handle are passed to the portal to prefill
+ * last used settings.
+ */
+void
+nsFlatpakPrintPortal::PreparePrint(GtkWindow* aWindow, const char* aWindowHandleStr)
+{
+  GtkPrintSettings* gtkSettings = mPrintAndPageSettings->GetGtkPrintSettings();
+  GtkPageSetup* pageSetup = mPrintAndPageSettings->GetGtkPageSetup();
+
+  // We need to remember GtkWindow to unexport window handle after it is
+  // no longer needed by the portal dialog (apply only on non-X11 sessions).
+  if (!GDK_IS_X11_DISPLAY(gdk_display_get_default())) {
+    mParentWindow = aWindow;
+  }
+
+  GVariantBuilder opt_builder;
+  g_variant_builder_init(&opt_builder, G_VARIANT_TYPE_VARDICT);
+  char* token = g_strdup_printf("mozilla%d", g_random_int_range (0, G_MAXINT));
+  g_variant_builder_add(&opt_builder, "{sv}", "handle_token",
+      g_variant_new_string(token));
+  g_free(token);
+  GVariant* options = g_variant_builder_end(&opt_builder);
+  static auto s_gtk_print_settings_to_gvariant =
+    reinterpret_cast<GVariant* (*)(GtkPrintSettings*)>
+    (dlsym(RTLD_DEFAULT, "gtk_print_settings_to_gvariant"));
+  static auto s_gtk_page_setup_to_gvariant =
+    reinterpret_cast<GVariant* (*)(GtkPageSetup *)>
+    (dlsym(RTLD_DEFAULT, "gtk_page_setup_to_gvariant"));
+  if (!s_gtk_print_settings_to_gvariant || !s_gtk_page_setup_to_gvariant) {
+    mResult = GTK_PRINT_OPERATION_RESULT_ERROR;
+    FinishPrintDialog(nullptr);
+    return;
+  }
+
+  // Get translated window title
+  nsCOMPtr<nsIStringBundleService> bundleSvc =
+       do_GetService(NS_STRINGBUNDLE_CONTRACTID);
+  nsCOMPtr<nsIStringBundle> printBundle;
+  bundleSvc->CreateBundle("chrome://global/locale/printdialog.properties",
+      getter_AddRefs(printBundle));
+  nsAutoString intlPrintTitle;
+  printBundle->GetStringFromName("printTitleGTK", intlPrintTitle);
+
+  GError* error = nullptr;
+  GVariant *ret = g_dbus_proxy_call_sync(mProxy,
+      "PreparePrint",
+      g_variant_new ("(ss@a{sv}@a{sv}@a{sv})",
+        aWindowHandleStr,
+        NS_ConvertUTF16toUTF8(intlPrintTitle).get(), // Title of the window
+        s_gtk_print_settings_to_gvariant(gtkSettings),
+        s_gtk_page_setup_to_gvariant(pageSetup),
+        options),
+      G_DBUS_CALL_FLAGS_NONE,
+      -1,
+      nullptr,
+      &error);
+  if (ret == nullptr) {
+    NS_WARNING(nsPrintfCString("Unable to call dbus proxy: %s", error->message).get());
+    g_error_free (error);
+    mResult = GTK_PRINT_OPERATION_RESULT_ERROR;
+    FinishPrintDialog(nullptr);
+    return;
+  }
+
+  const char* handle = nullptr;
+  g_variant_get (ret, "(&o)", &handle);
+  if (strcmp (aWindowHandleStr, handle) != 0)
+  {
+    aWindowHandleStr = g_strdup (handle);
+    g_dbus_connection_signal_unsubscribe(
+        g_dbus_proxy_get_connection(G_DBUS_PROXY(mProxy)), mResponseSignalId);
+  }
+  mResponseSignalId =
+    g_dbus_connection_signal_subscribe(
+        g_dbus_proxy_get_connection(G_DBUS_PROXY(mProxy)),
+        "org.freedesktop.portal.Desktop",
+        "org.freedesktop.portal.Request",
+        "Response",
+        aWindowHandleStr,
+        NULL,
+        G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
+        &nsFlatpakPrintPortal::OnPreparePrintResponse,
+        this, NULL);
+
+}
+
+void
+nsFlatpakPrintPortal::OnPreparePrintResponse(GDBusConnection *connection,
+                                             const char      *sender_name,
+                                             const char      *object_path,
+                                             const char      *interface_name,
+                                             const char      *signal_name,
+                                             GVariant        *parameters,
+                                             gpointer         data)
+{
+  nsFlatpakPrintPortal* printPortal = static_cast<nsFlatpakPrintPortal*>(data);
+  printPortal->FinishPrintDialog(parameters);
+}
+
+/**
+ * When the dialog is accepted, read print and page settings and token.
+ *
+ * Token is later used for printing portal as print operation identifier.
+ * Print and page settings are modified in-place and stored to
+ * mPrintAndPageSettings.
+ */
+void
+nsFlatpakPrintPortal::FinishPrintDialog(GVariant* parameters)
+{
+  // This ends GetResult() method
+  if (mLoop) {
+    g_main_loop_quit (mLoop);
+    mLoop = nullptr;
+  }
+
+  if (!parameters) {
+    // mResult should be already defined
+    return;
+  }
+
+  guint32 response;
+  GVariant *options;
+
+  g_variant_get (parameters, "(u@a{sv})", &response, &options);
+  mResult = GTK_PRINT_OPERATION_RESULT_CANCEL;
+  if (response == 0) {
+    GVariant *v;
+
+    char *filename;
+    char *uri;
+    v = g_variant_lookup_value (options, "settings", G_VARIANT_TYPE_VARDICT);
+    static auto s_gtk_print_settings_new_from_gvariant =
+      reinterpret_cast<GtkPrintSettings* (*)(GVariant*)>
+      (dlsym(RTLD_DEFAULT, "gtk_print_settings_new_from_gvariant"));
+
+    GtkPrintSettings* printSettings = s_gtk_print_settings_new_from_gvariant(v);
+    g_variant_unref (v);
+
+    v = g_variant_lookup_value (options, "page-setup", G_VARIANT_TYPE_VARDICT);
+    static auto s_gtk_page_setup_new_from_gvariant =
+      reinterpret_cast<GtkPageSetup* (*)(GVariant*)>
+      (dlsym(RTLD_DEFAULT, "gtk_page_setup_new_from_gvariant"));
+    GtkPageSetup* pageSetup = s_gtk_page_setup_new_from_gvariant(v);
+    g_variant_unref (v);
+
+    g_variant_lookup (options, "token", "u", &mToken);
+
+    // Force printing to file because only filedescriptor of the file
+    // can be passed to portal
+    int fd = g_file_open_tmp("gtkprintXXXXXX", &filename, NULL);
+    uri = g_filename_to_uri(filename, NULL, NULL);
+    gtk_print_settings_set(printSettings, GTK_PRINT_SETTINGS_OUTPUT_URI, uri);
+    g_free (uri);
+    close (fd);
+
+    // Save native settings in the session object
+    mPrintAndPageSettings->SetGtkPrintSettings(printSettings);
+    mPrintAndPageSettings->SetGtkPageSetup(pageSetup);
+
+    // Portal consumes PDF file
+    mPrintAndPageSettings->SetOutputFormat(nsIPrintSettings::kOutputFormatPDF);
+
+    // We need to set to print to file
+    mPrintAndPageSettings->SetPrintToFile(true);
+
+    mResult = GTK_PRINT_OPERATION_RESULT_APPLY;
+  }
+}
+
+/**
+ * Get result of the print dialog.
+ *
+ * This call blocks until FinishPrintDialog is called.
+ *
+ */
+GtkPrintOperationResult
+nsFlatpakPrintPortal::GetResult() {
+  // If the mLoop has not been initialized we haven't go thru PreparePrint method
+  if (!NS_IsMainThread() || !mLoop) {
+    return GTK_PRINT_OPERATION_RESULT_ERROR;
+  }
+  // Calling g_main_loop_run stops current code until g_main_loop_quit is called
+  g_main_loop_run(mLoop);
+
+  // Free resources we've allocated in order to show print dialog.
+#ifdef MOZ_WAYLAND
+  if (mParentWindow) {
+    GdkWindow *gdk_window = gtk_widget_get_window(GTK_WIDGET(mParentWindow));
+    static auto s_gdk_wayland_window_unexport_handle =
+      reinterpret_cast<void (*)(GdkWindow*)>
+      (dlsym(RTLD_DEFAULT, "gdk_wayland_window_unexport_handle"));
+    if (s_gdk_wayland_window_unexport_handle) {
+      s_gdk_wayland_window_unexport_handle(gdk_window);
+    }
+  }
+#endif
+  return mResult;
+}
+
+/**
+ * Send file descriptor of the file which contains document to the portal to
+ * finish the print operation.
+ */
+NS_IMETHODIMP
+nsFlatpakPrintPortal::Observe(nsISupports *aObject, const char * aTopic,
+                              const char16_t * aData)
+{
+  // Check that written file match to the stored filename in case multiple
+  // print operations are in progress.
+  nsAutoString filenameStr;
+  mPrintAndPageSettings->GetToFileName(filenameStr);
+  if (!nsDependentString(aData).Equals(filenameStr)) {
+    // Different file is finished, not for this instance
+    return NS_OK;
+  }
+  int fd, idx;
+  fd = open(NS_ConvertUTF16toUTF8(filenameStr).get(), O_RDONLY|O_CLOEXEC);
+  static auto s_g_unix_fd_list_new =
+    reinterpret_cast<GUnixFDList* (*)(void)>
+    (dlsym(RTLD_DEFAULT, "g_unix_fd_list_new"));
+  NS_ASSERTION(s_g_unix_fd_list_new, "Cannot find g_unix_fd_list_new function.");
+
+  GUnixFDList *fd_list = s_g_unix_fd_list_new();
+  static auto s_g_unix_fd_list_append =
+    reinterpret_cast<gint (*)(GUnixFDList*, gint, GError**)>
+    (dlsym(RTLD_DEFAULT, "g_unix_fd_list_append"));
+  idx = s_g_unix_fd_list_append(fd_list, fd, NULL);
+  close(fd);
+
+  GVariantBuilder opt_builder;
+  g_variant_builder_init(&opt_builder, G_VARIANT_TYPE_VARDICT);
+  g_variant_builder_add(&opt_builder, "{sv}",  "token",
+      g_variant_new_uint32(mToken));
+  g_dbus_proxy_call_with_unix_fd_list(
+      mProxy,
+      "Print",
+      g_variant_new("(ssh@a{sv})",
+                     "", /* window */
+                     "Print", /* title */
+                     idx,
+                     g_variant_builder_end(&opt_builder)),
+      G_DBUS_CALL_FLAGS_NONE,
+      -1,
+      fd_list,
+      NULL,
+      NULL, // TODO portal result cb function
+      nullptr); // data
+  g_object_unref(fd_list);
+
+  nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+  // Let the nsFlatpakPrintPortal instance die
+  os->RemoveObserver(this, "print-to-file-finished");
+  return NS_OK;
+}
+
+nsFlatpakPrintPortal::~nsFlatpakPrintPortal() {
+  if (mProxy)
+    g_object_unref(mProxy);
+  if (mLoop)
+    g_main_loop_quit(mLoop);
+}
+
 NS_IMETHODIMP
 nsPrintDialogServiceGTK::Show(nsPIDOMWindowOuter *aParent,
                               nsIPrintSettings *aSettings,
                               nsIWebBrowserPrint *aWebBrowserPrint)
 {
   NS_PRECONDITION(aParent, "aParent must not be null");
   NS_PRECONDITION(aSettings, "aSettings must not be null");
 
+  // Check for the flatpak portal first
+  nsCOMPtr<nsIGIOService> giovfs =
+    do_GetService(NS_GIOSERVICE_CONTRACTID);
+  bool shouldUsePortal;
+  giovfs->ShouldUseFlatpakPortal(&shouldUsePortal);
+  if (shouldUsePortal && gtk_check_version(3, 22, 0) == nullptr) {
+    nsCOMPtr<nsIWidget> widget = WidgetUtils::DOMWindowToWidget(aParent);
+    NS_ASSERTION(widget, "Need a widget for dialog to be modal.");
+    GtkWindow* gtkParent = get_gtk_window_for_nsiwidget(widget);
+    NS_ASSERTION(gtkParent, "Need a GTK window for dialog to be modal.");
+
+
+    nsCOMPtr<nsPrintSettingsGTK> printSettingsGTK(do_QueryInterface(aSettings));
+    RefPtr<nsFlatpakPrintPortal> fpPrintPortal =
+      new nsFlatpakPrintPortal(printSettingsGTK);
+
+    nsresult rv = fpPrintPortal->PreparePrintRequest(gtkParent);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    // This blocks until nsFlatpakPrintPortal::FinishPrintDialog is called
+    GtkPrintOperationResult printDialogResult = fpPrintPortal->GetResult();
+
+    rv = NS_OK;
+    switch (printDialogResult) {
+      case GTK_PRINT_OPERATION_RESULT_APPLY:
+        {
+          nsCOMPtr<nsIObserver> observer = do_QueryInterface(fpPrintPortal);
+          nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+          NS_ENSURE_STATE(os);
+          // Observer waits until notified that the file with the content
+          // to print has been written.
+          rv = os->AddObserver(observer, "print-to-file-finished", false);
+          NS_ENSURE_SUCCESS(rv, rv);
+          break;
+        }
+      case GTK_PRINT_OPERATION_RESULT_CANCEL:
+        rv = NS_ERROR_ABORT;
+        break;
+      default:
+        NS_WARNING("Unexpected response");
+        rv = NS_ERROR_ABORT;
+    }
+    return rv;
+  }
+
   nsPrintDialogWidgetGTK printDialog(aParent, aSettings);
   nsresult rv = printDialog.ImportSettings(aSettings);
 
   NS_ENSURE_SUCCESS(rv, rv);
 
   const gint response = printDialog.Run();
 
   // Handle the result