Blob Blame History Raw
/* nma-bar-code-widget.h - Renderer of a "QR" code
 *
 * Lubomir Rintel <lkundrak@v3.sk>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public License as
 * published by the ree Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Copyright 2018, 2019 Red Hat, Inc.
 */

#include "nm-default.h"

#include <stdint.h>

#include "nma-bar-code.h"
#include "nma-bar-code-widget.h"

#define CARD_WIDTH_PT 252
#define CARD_HEIGHT_PT 144

struct _NMABarCodeWidget {
	GtkBox parent;
};

struct _NMABarCodeWidgetClass {
	GtkBoxClass parent_class;
};

typedef struct {
	NMConnection *connection;
	GtkWidget *qr_code;
	NMABarCode *qr;
} NMABarCodeWidgetPrivate;

/**
 * SECTION:nma-bar-code-widget
 * @title: NMABarCodeWidget
 *
 * This is a widget that displays a QR code for a connection suitable for
 * optical recognition, e.g. scanning on a phone to connect to a hotspot.
 */

G_DEFINE_TYPE_WITH_CODE (NMABarCodeWidget, nma_bar_code_widget, GTK_TYPE_BOX,
                         G_ADD_PRIVATE (NMABarCodeWidget))

enum {
	PROP_0,
	PROP_CONNECTION,

	LAST_PROP
};

#define NMA_BAR_CODE_WIDGET_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), \
                                     NMA_TYPE_BAR_CODE_WIDGET, \
                                     NMABarCodeWidgetPrivate))

static void
do_qr_code_draw (GtkDrawingArea *area, cairo_t *cr, int width, int height,
                 gpointer user_data)
{
	NMABarCodeWidgetPrivate *priv = NMA_BAR_CODE_WIDGET_GET_PRIVATE (user_data);
	int size = nma_bar_code_get_size (priv->qr);

	gtk_widget_set_size_request (priv->qr_code, (size + 2) * 3, (size + 2) * 3);

	cairo_set_source_rgba (cr, 1, 1, 1, 1);
	cairo_fill (cr);
	cairo_paint (cr);
	cairo_set_source_rgba (cr, 0, 0, 0, 1);
	cairo_scale (cr, (float)width / (size + 2), (float)height / (size + 2));
	cairo_translate (cr, 1, 1);
	cairo_set_antialias (cr, CAIRO_ANTIALIAS_NONE);
	nma_bar_code_draw (priv->qr, cr);
}

static gboolean
qr_code_draw (GtkWidget *widget, cairo_t *cr, gpointer user_data)
{
	do_qr_code_draw (GTK_DRAWING_AREA (widget), cr,
	                 gtk_widget_get_allocated_width (widget),
	                 gtk_widget_get_allocated_height (widget),
	                 user_data);

	return TRUE;
}

static char *
shell_escape (const char *to_escape)
{
	GString *string = g_string_sized_new (32);
	gboolean quote = *to_escape == '\0';
	const char *c;

	for (c = to_escape; *c; c++) {
		if (strchr ("$\\\"", *c))
			g_string_append_c (string, '\\');
		else if (!g_ascii_isalnum(*c) && !strchr ("@%^+-_[]:", *c))
			quote = TRUE;
		g_string_append_c (string, *c);
	}
	if (quote) {
		g_string_append_c (string, '"');
		g_string_prepend_c (string, '"');
	}
	return g_string_free (string, FALSE);
}

static void
draw_one (NMABarCodeWidget *self, cairo_t *cr,
          const char *psk, const char *ssid,
          const char *nmcli_line1, const char *nmcli_line2)
{
	NMABarCodeWidgetPrivate *priv = NMA_BAR_CODE_WIDGET_GET_PRIVATE (self);
	int size = nma_bar_code_get_size (priv->qr);

	cairo_save (cr);

	cairo_set_line_width (cr, 0.01);
	cairo_rectangle (cr, 0, 0, CARD_WIDTH_PT, CARD_HEIGHT_PT);
	cairo_stroke (cr);
	cairo_translate (cr, 12, 12);

	cairo_save (cr);
	cairo_scale (cr, (float)84/(float)size, (float)84/(float)size);
	cairo_set_antialias (cr, CAIRO_ANTIALIAS_NONE);
	nma_bar_code_draw (priv->qr, cr);
	cairo_restore (cr);

	cairo_select_font_face(cr, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);

	cairo_move_to (cr, 96, 12);
	cairo_set_font_size (cr, 12);
	cairo_show_text(cr, _("Network"));

	cairo_move_to (cr, 96, 30);
	cairo_set_font_size (cr, 16);
	cairo_show_text(cr, ssid);

	cairo_select_font_face(cr, "Monospace", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);

	cairo_move_to (cr, 0, 108);
	cairo_set_font_size (cr, 10);
	cairo_show_text(cr, nmcli_line1);

	if (psk) {
		cairo_select_font_face(cr, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);

		cairo_move_to (cr, 96, 60);
		cairo_set_font_size (cr, 12);
		cairo_show_text(cr, _("Password"));

		cairo_move_to (cr, 96, 78);
		cairo_set_font_size (cr, 16);
		cairo_show_text(cr, psk);

		cairo_select_font_face(cr, "Monospace", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);

		cairo_move_to (cr, 24, 120);
		cairo_set_font_size (cr, 10);
		cairo_show_text(cr, nmcli_line2);
	}

	cairo_restore (cr);
}

static void
draw_page (GtkPrintOperation *operation, GtkPrintContext *context, int page_nr, gpointer user_data)
{
	NMABarCodeWidget *self = NMA_BAR_CODE_WIDGET (user_data);
	NMABarCodeWidgetPrivate *priv = NMA_BAR_CODE_WIDGET_GET_PRIVATE (self);
	cairo_t *cr = gtk_print_context_get_cairo_context (context);
	double width = gtk_print_context_get_width (context);
	double height = gtk_print_context_get_height (context);
	int count_x = width / CARD_WIDTH_PT;
	int count_y = height / CARD_HEIGHT_PT;
	double spacing_x = (width - (count_x * CARD_WIDTH_PT)) / (count_x + 1);
	double spacing_y = (height - (count_y * CARD_HEIGHT_PT)) / (count_y + 1);
	NMSettingWireless *s_wireless;
	NMSettingWirelessSecurity *s_wsec;
	gs_free char *nmcli_line1 = NULL;
	gs_free char *nmcli_line2 = NULL;
	const char *psk = NULL;
	GBytes *ssid_bytes;
	char *ssid;
	char *tmp;
	int x, y;

	s_wireless = nm_connection_get_setting_wireless (priv->connection);
	if (!s_wireless) {
		nma_bar_code_set_text (priv->qr, NULL);
		gtk_widget_queue_draw (priv->qr_code);
		return;
	}

	ssid_bytes = nm_setting_wireless_get_ssid (s_wireless);
	g_return_if_fail (ssid_bytes);
	ssid = nm_utils_ssid_to_utf8 (g_bytes_get_data (ssid_bytes, NULL),
	                              g_bytes_get_size (ssid_bytes));
	g_return_if_fail (ssid);

	s_wsec = nm_connection_get_setting_wireless_security (priv->connection);
	if (s_wsec)
		psk = nm_setting_wireless_security_get_psk (s_wsec);

	tmp = shell_escape (ssid);
	nmcli_line1 = g_strdup_printf ("$ nmcli d wifi con %s%s", tmp, psk ? " \\" : "");
	g_free (tmp);

	if (psk) {
		tmp = shell_escape (psk);
		nmcli_line2 = g_strdup_printf ("password %s", tmp);
		g_free (tmp);
	}

	for (y = 0; y < count_y; y++) {
		cairo_save (cr);

		cairo_translate (cr, spacing_x, spacing_y);

		for (x = 0; x < count_x; x++) {
			draw_one (self, cr, psk, ssid, nmcli_line1, nmcli_line2);
			cairo_translate (cr, CARD_WIDTH_PT + spacing_x, 0);
		}

		cairo_restore (cr);
		cairo_translate (cr, 0, CARD_HEIGHT_PT + spacing_y);
	}
}

static gboolean
link_activated (GtkLabel *label, char *uri, gpointer user_data)
{
	NMABarCodeWidget *self = NMA_BAR_CODE_WIDGET (user_data);
	GtkPrintOperation *print = gtk_print_operation_new ();
	GtkWidget *window;
	GError *error = NULL;

	g_return_val_if_fail (strcmp (uri, "nma:print") == 0, FALSE);

	window = gtk_widget_get_ancestor (GTK_WIDGET (self), GTK_TYPE_WINDOW);

	gtk_print_operation_set_n_pages (print, 1);
	gtk_print_operation_set_use_full_page (print, TRUE);
	gtk_print_operation_set_unit (print, GTK_UNIT_POINTS);
	g_signal_connect (print, "draw_page", G_CALLBACK (draw_page), self);

	if (!gtk_print_operation_run (print, GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG,
	                              window ? GTK_WINDOW (window) : NULL, &error)) {
		g_printerr ("%s", error->message);
		g_error_free (error);
	}

	g_object_unref (print);

	return FALSE;
}

static void
string_append_mecard (GString *string, const char *tag, const char *text)
{
	const char *p;
	bool is_hex = TRUE;
	int start;

	if (!text)
		return;

	g_string_append (string, tag);
	start = string->len;

	for (p = text; *p; p++) {
		if (!g_ascii_isxdigit (*p))
			is_hex = FALSE;
		if (strchr ("\\\":;,", *p))
			g_string_append_c (string, '\\');
		g_string_append_c (string, *p);
	}

	if (is_hex) {
		g_string_insert_c (string, start, '\"');
		g_string_append_c (string, '\"');
	}
	g_string_append_c (string, ';');
}

static void
update_qr_code (NMABarCodeWidget *self)
{
	NMABarCodeWidgetPrivate *priv = NMA_BAR_CODE_WIDGET_GET_PRIVATE (self);
	NMSettingWireless *s_wireless;
	NMSettingWirelessSecurity *s_wsec;
	const char *key_mgmt = NULL;
	const char *psk = NULL;
	const char *type = NULL;
	GBytes *ssid_bytes;
	char *ssid;
	GString *string;

	if (!priv->qr)
		return;

	s_wireless = nm_connection_get_setting_wireless (priv->connection);
	if (!s_wireless) {
		nma_bar_code_set_text (priv->qr, NULL);
		gtk_widget_queue_draw (priv->qr_code);
		return;
	}

	ssid_bytes = nm_setting_wireless_get_ssid (s_wireless);
	g_return_if_fail (ssid_bytes);
	ssid = nm_utils_ssid_to_utf8 (g_bytes_get_data (ssid_bytes, NULL),
	                              g_bytes_get_size (ssid_bytes));
	g_return_if_fail (ssid);

	string = g_string_sized_new (64);
	g_string_append (string, "WIFI:");

	s_wsec = nm_connection_get_setting_wireless_security (priv->connection);
	if (s_wsec) {
		key_mgmt = nm_setting_wireless_security_get_key_mgmt (s_wsec);
		psk = nm_setting_wireless_security_get_psk (s_wsec);
	}

	if (key_mgmt == NULL) {
		type = "nopass";
	} else if (   strcmp (key_mgmt, "none") == 0
	           || strcmp (key_mgmt, "ieee8021x") == 0) {
		type = "WEP";
	} else if (   strcmp (key_mgmt, "wpa-none") == 0
	           || strcmp (key_mgmt, "wpa-psk") == 0) {
		type = "WPA";
	}

	string_append_mecard(string, "T:", type);
	string_append_mecard(string, "S:", ssid);
	string_append_mecard(string, "P:", psk);

	if (nm_setting_wireless_get_hidden (s_wireless))
		g_string_append (string, "H:true;");

	g_string_append_c (string, ';');
	nma_bar_code_set_text (priv->qr, string->str);
	gtk_widget_queue_draw (priv->qr_code);
	g_string_free (string, TRUE);
}

static void
set_connection (NMABarCodeWidget *self, NMConnection *connection)
{
	NMABarCodeWidgetPrivate *priv = NMA_BAR_CODE_WIDGET_GET_PRIVATE (self);

	if (priv->connection) {
		g_signal_handlers_disconnect_by_data (priv->connection, self);
		g_clear_object (&priv->connection);
	}

	if (connection) {
		priv->connection = connection;
		g_signal_connect_swapped (connection, "changed", G_CALLBACK (update_qr_code), self);
		g_signal_connect_swapped (connection, "secrets-updated", G_CALLBACK (update_qr_code), self);
	}
}

static void
get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
	NMABarCodeWidgetPrivate *priv = NMA_BAR_CODE_WIDGET_GET_PRIVATE (object);

	switch (prop_id) {
	case PROP_CONNECTION:
		g_value_set_object (value, priv->connection);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

static void
set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
{
	NMABarCodeWidget *self = NMA_BAR_CODE_WIDGET (object);

	switch (prop_id) {
	case PROP_CONNECTION:
		set_connection (self, g_value_dup_object (value));
		update_qr_code (self);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

static void
nma_bar_code_widget_init (NMABarCodeWidget *self)
{
	NMABarCodeWidgetPrivate *priv = NMA_BAR_CODE_WIDGET_GET_PRIVATE (self);

	gtk_widget_init_template (GTK_WIDGET (self));

	priv->qr = nma_bar_code_new (NULL);
	g_signal_connect (priv->qr_code, "draw", G_CALLBACK (qr_code_draw), self);
}

/**
 * nma_bar_code_widget_new:
 * @connection: connection to get network details from
 *
 * Returns: (transfer full): the bar code widget instance
 *
 * Since: 1.8.22
 */
GtkWidget *
nma_bar_code_widget_new (NMConnection *connection)
{
	return g_object_new (NMA_TYPE_BAR_CODE_WIDGET,
	                     NMA_BAR_CODE_WIDGET_CONNECTION, connection,
	                     NULL);
}

static void
finalize (GObject *object)
{
	NMABarCodeWidget *self = NMA_BAR_CODE_WIDGET (object);
	NMABarCodeWidgetPrivate *priv = NMA_BAR_CODE_WIDGET_GET_PRIVATE (self);

	g_clear_object (&priv->qr);
	set_connection (self, NULL);

	G_OBJECT_CLASS (nma_bar_code_widget_parent_class)->finalize (object);
}

static void
nma_bar_code_widget_class_init (NMABarCodeWidgetClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

	object_class->get_property = get_property;
	object_class->set_property = set_property;
	object_class->finalize = finalize;

	g_object_class_install_property
		(object_class, PROP_CONNECTION,
		 g_param_spec_object (NMA_BAR_CODE_WIDGET_CONNECTION, "", "",
		                      NM_TYPE_CONNECTION,
		                      G_PARAM_READABLE | G_PARAM_WRITABLE));

	gtk_widget_class_set_template_from_resource (widget_class,
	                                             "/org/freedesktop/network-manager-applet/nma-bar-code-widget.ui");

	gtk_widget_class_bind_template_child_private (widget_class, NMABarCodeWidget, qr_code);
	gtk_widget_class_bind_template_callback (widget_class, link_activated);
}