1 /*
2  * Claws Mail -- A GTK+ based, lightweight, and fast e-mail client
3  * Copyright(C) 2019 the Claws Mail Team
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 3 of the License, or
8  * (at your option) any later version.
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License for more details.
13  * You should have received a copy of the GNU General Public License
14  * along with this program; if not, write tothe Free Software
15  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16  */
17 
18 #ifdef HAVE_CONFIG_H
19 #include "config.h"
20 #include "claws-features.h"
21 #endif
22 
23 #include <glib.h>
24 #include <glib/gi18n.h>
25 #include <glib/gstdio.h>
26 #include <fcntl.h>
27 #include <sys/types.h>
28 #include <sys/stat.h>
29 #include <curl/curl.h>
30 #include <gdk/gdk.h>
31 
32 #include "utils.h"
33 
34 #include "litehtml/litehtml.h"
35 
36 #include "lh_prefs.h"
37 #include "lh_widget.h"
38 #include "lh_widget_wrapped.h"
39 
40 extern "C" {
41 const gchar *prefs_common_get_uri_cmd(void);
42 }
43 
44 char master_css[] = {
45 #include "css.inc"
46 };
47 
48 static gboolean expose_event_cb(GtkWidget *widget, GdkEvent *event,
49 		gpointer user_data);
50 static gboolean button_press_event(GtkWidget *widget, GdkEventButton *event,
51 		gpointer user_data);
52 static gboolean motion_notify_event(GtkWidget *widget, GdkEventButton *event,
53         gpointer user_data);
54 static gboolean button_release_event(GtkWidget *widget, GdkEventButton *event,
55         gpointer user_data);
56 static void open_link_cb(GtkMenuItem *item, gpointer user_data);
57 static void copy_link_cb(GtkMenuItem *item, gpointer user_data);
58 
lh_widget()59 lh_widget::lh_widget()
60 {
61 	GtkWidget *item;
62 
63 	/* scrolled window */
64 	m_scrolled_window = gtk_scrolled_window_new(NULL, NULL);
65 	gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(m_scrolled_window),
66 			GTK_POLICY_AUTOMATIC, GTK_POLICY_ALWAYS);
67 
68 	/* viewport */
69 	GtkScrolledWindow *scw = GTK_SCROLLED_WINDOW(m_scrolled_window);
70 	m_viewport = gtk_viewport_new(
71 			gtk_scrolled_window_get_hadjustment(scw),
72 			gtk_scrolled_window_get_vadjustment(scw));
73 	gtk_container_add(GTK_CONTAINER(m_scrolled_window), m_viewport);
74 
75 	/* drawing area */
76 	m_drawing_area = gtk_drawing_area_new();
77 	gtk_container_add(GTK_CONTAINER(m_viewport), m_drawing_area);
78 	g_signal_connect(m_drawing_area, "expose-event",
79 			G_CALLBACK(expose_event_cb), this);
80 	g_signal_connect(m_drawing_area, "motion_notify_event",
81 			G_CALLBACK(motion_notify_event), this);
82 	g_signal_connect(m_drawing_area, "button_press_event",
83 			G_CALLBACK(button_press_event), this);
84 	g_signal_connect(m_drawing_area, "button_release_event",
85 			G_CALLBACK(button_release_event), this);
86 
87 	gtk_widget_show_all(m_scrolled_window);
88 
89 	/* context menu */
90 	m_context_menu = gtk_menu_new();
91 
92 	item = gtk_menu_item_new_with_label(_("Open Link"));
93 	g_signal_connect(G_OBJECT(item), "activate", G_CALLBACK(open_link_cb), this);
94 	gtk_menu_shell_append(GTK_MENU_SHELL(m_context_menu), item);
95 
96 	item = gtk_menu_item_new_with_label(_("Copy Link Location"));
97 	g_signal_connect(G_OBJECT(item), "activate", G_CALLBACK(copy_link_cb), this);
98 	gtk_menu_shell_append(GTK_MENU_SHELL(m_context_menu), item);
99 
100 	m_html = NULL;
101 	m_rendered_width = 0;
102 	m_context.load_master_stylesheet(master_css);
103 
104 	m_font_name = NULL;
105 	m_font_size = 0;
106 
107 	m_partinfo = NULL;
108 
109 	m_showing_url = FALSE;
110 
111 	gtk_widget_set_events(m_drawing_area,
112 			        GDK_BUTTON_RELEASE_MASK
113 			      | GDK_BUTTON_PRESS_MASK
114 			      | GDK_POINTER_MOTION_MASK);
115 }
116 
~lh_widget()117 lh_widget::~lh_widget()
118 {
119 	g_object_unref(m_drawing_area);
120 	m_drawing_area = NULL;
121 	g_object_unref(m_scrolled_window);
122 	m_scrolled_window = NULL;
123 	m_html = NULL;
124 	g_free(m_font_name);
125 }
126 
get_widget() const127 GtkWidget *lh_widget::get_widget() const
128 {
129 	return m_scrolled_window;
130 }
131 
set_caption(const litehtml::tchar_t * caption)132 void lh_widget::set_caption(const litehtml::tchar_t* caption)
133 {
134 	debug_print("lh_widget set_caption\n");
135 	return;
136 }
137 
set_base_url(const litehtml::tchar_t * base_url)138 void lh_widget::set_base_url(const litehtml::tchar_t* base_url)
139 {
140 	debug_print("lh_widget set_base_url '%s'\n",
141 			(base_url ? base_url : "(null)"));
142 	if (base_url)
143 		m_base_url = base_url;
144 	else
145 		m_base_url.clear();
146 
147 	return;
148 }
149 
on_anchor_click(const litehtml::tchar_t * url,const litehtml::element::ptr & el)150 void lh_widget::on_anchor_click(const litehtml::tchar_t* url, const litehtml::element::ptr& el)
151 {
152 	debug_print("lh_widget on_anchor_click. url -> %s\n", url);
153 
154 	m_clicked_url = fullurl(url);
155 	return;
156 }
157 
import_css(litehtml::tstring & text,const litehtml::tstring & url,litehtml::tstring & baseurl)158 void lh_widget::import_css(litehtml::tstring& text, const litehtml::tstring& url, litehtml::tstring& baseurl)
159 {
160 	debug_print("lh_widget import_css\n");
161 	baseurl = master_css;
162 }
163 
get_client_rect(litehtml::position & client) const164 void lh_widget::get_client_rect(litehtml::position& client) const
165 {
166 	if (m_drawing_area == NULL)
167 		return;
168 
169 	client.width = m_rendered_width;
170 	client.height = m_height;
171 	client.x = 0;
172 	client.y = 0;
173 
174 //	debug_print("lh_widget::get_client_rect: %dx%d\n",
175 //			client.width, client.height);
176 }
177 
open_html(const gchar * contents)178 void lh_widget::open_html(const gchar *contents)
179 {
180 	gint num = clear_images(lh_prefs_get()->image_cache_size * 1024 * 1000);
181 	GtkAdjustment *adj;
182 
183 	debug_print("LH: cleared %d images from image cache\n", num);
184 
185 	update_font();
186 
187 	lh_widget_statusbar_push("Loading HTML part ...");
188 	m_html = litehtml::document::createFromString(contents, this, &m_context);
189 	m_rendered_width = 0;
190 	if (m_html != NULL) {
191 		debug_print("lh_widget::open_html created document\n");
192 		adj = gtk_scrolled_window_get_hadjustment(
193 				GTK_SCROLLED_WINDOW(m_scrolled_window));
194 		gtk_adjustment_set_value(adj, 0.0);
195 		adj = gtk_scrolled_window_get_vadjustment(
196 				GTK_SCROLLED_WINDOW(m_scrolled_window));
197 		gtk_adjustment_set_value(adj, 0.0);
198 		redraw(false);
199 	}
200 	lh_widget_statusbar_pop();
201 }
202 
draw(cairo_t * cr)203 void lh_widget::draw(cairo_t *cr)
204 {
205 	double x1, x2, y1, y2;
206 	double width, height;
207 
208 	if (m_html == NULL)
209 		return;
210 
211 	cairo_clip_extents(cr, &x1, &y1, &x2, &y2);
212 
213 	width = x2 - x1;
214 	height = y2 - y1;
215 
216 	litehtml::position pos;
217 	pos.width = (int)width;
218 	pos.height = (int)height;
219 	pos.x = (int)x1;
220 	pos.y = (int)y1;
221 
222 	m_html->draw((litehtml::uint_ptr)cr, 0, 0, &pos);
223 }
224 
redraw(gboolean force_render)225 void lh_widget::redraw(gboolean force_render)
226 {
227 	GtkAllocation rect;
228 	gint width;
229 	GdkWindow *gdkwin;
230 	cairo_t *cr;
231 
232 	paint_white();
233 
234 	if (m_html == NULL)
235 		return;
236 
237 	/* Get width of the viewport. */
238 	gdkwin = gtk_viewport_get_view_window(GTK_VIEWPORT(m_viewport));
239 	width = gdk_window_get_width(gdkwin);
240 	m_height = gdk_window_get_height(gdkwin);
241 
242 	/* If the available width has changed, rerender the HTML content. */
243 	if (m_rendered_width != width || force_render) {
244 		debug_print("lh_widget::redraw: width changed: %d != %d\n",
245 				m_rendered_width, width);
246 
247 		/* Update our internally stored width, mainly so that
248 		 * lh_widget::get_client_rect() gives correct width during the
249 		 * render. */
250 		m_rendered_width = width;
251 
252 		/* Re-render HTML for this width. */
253 		m_html->media_changed();
254 		m_html->render(m_rendered_width);
255 		debug_print("render is %dx%d\n", m_html->width(), m_html->height());
256 
257 		/* Change drawing area's size to match what was rendered. */
258 		gtk_widget_set_size_request(m_drawing_area,
259 				m_html->width(), m_html->height());
260 	}
261 
262 	/* Paint the rendered HTML. */
263 	gdkwin = gtk_widget_get_window(m_drawing_area);
264 	if (gdkwin == NULL) {
265 		g_warning("lh_widget::redraw: No GdkWindow to draw on!");
266 		return;
267 	}
268 	cr = gdk_cairo_create(GDK_DRAWABLE(gdkwin));
269 	draw(cr);
270 
271 	cairo_destroy(cr);
272 }
273 
paint_white()274 void lh_widget::paint_white()
275 {
276 	GdkWindow *gdkwin = gtk_widget_get_window(m_drawing_area);
277 	if (gdkwin == NULL) {
278 		g_warning("lh_widget::clear: No GdkWindow to draw on!");
279 		return;
280 	}
281 	cairo_t *cr = gdk_cairo_create(GDK_DRAWABLE(gdkwin));
282 
283 	/* Paint white background. */
284 	gint width, height;
285 	gdk_drawable_get_size(gdkwin, &width, &height);
286 	cairo_rectangle(cr, 0, 0, width, height);
287 	cairo_set_source_rgb(cr, 255, 255, 255);
288 	cairo_fill(cr);
289 
290 	cairo_destroy(cr);
291 }
clear()292 void lh_widget::clear()
293 {
294 	m_html = nullptr;
295 	paint_white();
296 	m_rendered_width = 0;
297 	m_base_url.clear();
298 	m_clicked_url.clear();
299 }
300 
set_cursor(const litehtml::tchar_t * cursor)301 void lh_widget::set_cursor(const litehtml::tchar_t* cursor)
302 {
303 	litehtml::element::ptr over_el = m_html->over_element();
304 	gint x, y;
305 
306 	if (m_showing_url &&
307 			(over_el == NULL || over_el != m_over_element)) {
308 		lh_widget_statusbar_pop();
309 		m_showing_url = FALSE;
310 	}
311 
312 	if (over_el != m_over_element) {
313 		m_over_element = over_el;
314 		update_cursor(cursor);
315 	}
316 }
317 
update_cursor(const litehtml::tchar_t * cursor)318 void lh_widget::update_cursor(const litehtml::tchar_t* cursor)
319 {
320 	GdkCursorType cursType = GDK_ARROW;
321 	const litehtml::tchar_t *href = get_href_at(m_over_element);
322 
323 	/* If there is a href, and litehtml is okay with showing a pointer
324 	 * cursor ("pointer" or "auto"), set it, otherwise keep the
325 	 * default arrow cursor */
326 	if ((!strcmp(cursor, "pointer") || !strcmp(cursor, "auto")) &&
327 			href != NULL) {
328 		cursType = GDK_HAND2;
329 	}
330 
331 	if (cursType == GDK_ARROW) {
332 		gdk_window_set_cursor(gtk_widget_get_window(m_drawing_area), NULL);
333 	} else {
334 		gdk_window_set_cursor(gtk_widget_get_window(m_drawing_area), gdk_cursor_new(cursType));
335 	}
336 
337 	/* If there is a href, show it in statusbar */
338 	if (href != NULL) {
339 		lh_widget_statusbar_push(fullurl(href).c_str());
340 		m_showing_url = TRUE;
341 	}
342 }
343 
get_href_at(litehtml::element::ptr element) const344 const litehtml::tchar_t *lh_widget::get_href_at(litehtml::element::ptr element) const
345 {
346 	litehtml::element::ptr el;
347 
348 	if (element == NULL)
349 		return NULL;
350 
351 	/* If it's not an anchor, check if it has a parent anchor
352 	 * (e.g. it's an image within an anchor) and grab a pointer
353 	 * to that. */
354 	if (strcmp(element->get_tagName(), "a") && element->parent()) {
355 		el = element->parent();
356 		while (el && el != m_html->root() && strcmp(el->get_tagName(), "a")) {
357 			el = el->parent();
358 		}
359 
360 		if (!el || el == m_html->root())
361 			return NULL;
362 	} else {
363 		el = element;
364 	}
365 
366 	/* At this point, over_el is pointing at an anchor tag, so let's
367 	 * grab its href attribute. */
368 	return el->get_attr(_t("href"));
369 }
370 
get_href_at(const gint x,const gint y) const371 const litehtml::tchar_t *lh_widget::get_href_at(const gint x, const gint y) const
372 {
373 	litehtml::element::ptr over_el, el;
374 
375 	if (m_html == NULL)
376 		return NULL;
377 
378 	over_el = m_html->root()->get_element_by_point(x, y, x, y);
379 	if (over_el == NULL)
380 		return NULL;
381 
382 	return get_href_at(over_el);
383 }
384 
print()385 void lh_widget::print()
386 {
387     debug_print("lh_widget print\n");
388     gtk_widget_realize(GTK_WIDGET(m_drawing_area));
389 }
390 
popup_context_menu(const litehtml::tchar_t * url,GdkEventButton * event)391 void lh_widget::popup_context_menu(const litehtml::tchar_t *url,
392 		GdkEventButton *event)
393 {
394 	cm_return_if_fail(url != NULL);
395 	cm_return_if_fail(event != NULL);
396 
397 	debug_print("lh_widget showing context menu for '%s'\n", url);
398 
399 	m_clicked_url = url;
400 	gtk_widget_show_all(m_context_menu);
401 	gtk_menu_popup(GTK_MENU(m_context_menu), NULL, NULL, NULL, NULL,
402 			event->button, event->time);
403 }
404 
update_font()405 void lh_widget::update_font()
406 {
407 	PangoFontDescription *pd =
408 		pango_font_description_from_string(lh_prefs_get()->default_font);
409 	gboolean absolute = pango_font_description_get_size_is_absolute(pd);
410 
411 	g_free(m_font_name);
412 	m_font_name = g_strdup(pango_font_description_get_family(pd));
413 	m_font_size = pango_font_description_get_size(pd);
414 
415 	pango_font_description_free(pd);
416 
417 	if (!absolute)
418 		m_font_size /= PANGO_SCALE;
419 
420 	debug_print("Font set to '%s', size %d\n", m_font_name, m_font_size);
421 }
422 
fullurl(const litehtml::tchar_t * url) const423 const litehtml::tstring lh_widget::fullurl(const litehtml::tchar_t *url) const
424 {
425 	if (*url == '#' && !m_base_url.empty())
426 		return m_base_url + url;
427 
428 	return _t(url);
429 }
430 
set_partinfo(MimeInfo * partinfo)431 void lh_widget::set_partinfo(MimeInfo *partinfo)
432 {
433 	m_partinfo = partinfo;
434 }
435 
get_local_image(const litehtml::tstring url) const436 GdkPixbuf *lh_widget::get_local_image(const litehtml::tstring url) const
437 {
438 	GdkPixbuf *pixbuf;
439 	const gchar *name;
440 	MimeInfo *p = m_partinfo;
441 
442 	if (strncmp(url.c_str(), "cid:", 4) != 0) {
443 		debug_print("lh_widget::get_local_image: '%s' is not a local URI, ignoring\n", url.c_str());
444 		return NULL;
445 	}
446 
447 	name = url.c_str() + 4;
448 	debug_print("getting message part '%s'\n", name);
449 
450 	while ((p = procmime_mimeinfo_next(p)) != NULL) {
451 		size_t len = strlen(name);
452 
453 		/* p->id is in format "<partname>" */
454 		if (p->id != NULL &&
455 				strlen(p->id) >= len + 2 &&
456 				!strncasecmp(name, p->id + 1, len) &&
457 				*(p->id + len + 1) == '>') {
458 			GError *error = NULL;
459 
460 			pixbuf = procmime_get_part_as_pixbuf(p, &error);
461 			if (error != NULL) {
462 				g_warning("Couldn't load image: %s\n", error->message);
463 				g_error_free(error);
464 				return NULL;
465 			}
466 
467 			return pixbuf;
468 		}
469 	}
470 
471 	/* MIME part with requested name was not found */
472 	return NULL;
473 }
474 
475 ////////////////////////////////////////////////
expose_event_cb(GtkWidget * widget,GdkEvent * event,gpointer user_data)476 static gboolean expose_event_cb(GtkWidget *widget, GdkEvent *event,
477 		gpointer user_data)
478 {
479 	lh_widget *w = (lh_widget *)user_data;
480 	w->redraw(false);
481 	return FALSE;
482 }
483 
button_press_event(GtkWidget * widget,GdkEventButton * event,gpointer user_data)484 static gboolean button_press_event(GtkWidget *widget, GdkEventButton *event,
485 		gpointer user_data)
486 {
487 	litehtml::position::vector redraw_boxes;
488 	lh_widget *w = (lh_widget *)user_data;
489 
490 	if (w->m_html == NULL)
491 		return false;
492 
493 	//debug_print("lh_widget on_button_press_event\n");
494 
495 	if (event->type == GDK_2BUTTON_PRESS ||
496 			event->type == GDK_3BUTTON_PRESS)
497 		return true;
498 
499 	/* Right-click */
500 	if (event->button == 3) {
501 		const litehtml::tchar_t *url = w->get_href_at((gint)event->x, (gint)event->y);
502 
503 		if (url != NULL)
504 			w->popup_context_menu(url, event);
505 
506 		return true;
507 	}
508 
509 	if(w->m_html->on_lbutton_down((int) event->x, (int) event->y,
510 				(int) event->x, (int) event->y, redraw_boxes)) {
511 		for(auto& pos : redraw_boxes) {
512 			debug_print("x: %d y:%d w: %d h: %d\n", pos.x, pos.y, pos.width, pos.height);
513 			gtk_widget_queue_draw_area(widget, pos.x, pos.y, pos.width, pos.height);
514 		}
515 	}
516 
517 	return true;
518 }
519 
motion_notify_event(GtkWidget * widget,GdkEventButton * event,gpointer user_data)520 static gboolean motion_notify_event(GtkWidget *widget, GdkEventButton *event,
521         gpointer user_data)
522 {
523     litehtml::position::vector redraw_boxes;
524     lh_widget *w = (lh_widget *)user_data;
525 
526     //debug_print("lh_widget on_motion_notify_event\n");
527 
528     if(w->m_html)
529     {
530         if(w->m_html->on_mouse_over((int) event->x, (int) event->y, (int) event->x, (int) event->y, redraw_boxes))
531         {
532             for (auto& pos : redraw_boxes)
533             {
534 		debug_print("x: %d y:%d w: %d h: %d\n", pos.x, pos.y, pos.width, pos.height);
535                 gtk_widget_queue_draw_area(widget, pos.x, pos.y, pos.width, pos.height);
536             }
537         }
538 	}
539 
540 	return true;
541 }
542 
button_release_event(GtkWidget * widget,GdkEventButton * event,gpointer user_data)543 static gboolean button_release_event(GtkWidget *widget, GdkEventButton *event,
544         gpointer user_data)
545 {
546     litehtml::position::vector redraw_boxes;
547     lh_widget *w = (lh_widget *)user_data;
548     GError* error = NULL;
549 
550 	if (w->m_html == NULL)
551 		return false;
552 
553 	//debug_print("lh_widget on_button_release_event\n");
554 
555 	if (event->type == GDK_2BUTTON_PRESS ||
556 			event->type == GDK_3BUTTON_PRESS)
557 		return true;
558 
559 	/* Right-click */
560 	if (event->button == 3)
561 		return true;
562 
563 	w->m_clicked_url.clear();
564 
565     if(w->m_html->on_lbutton_up((int) event->x, (int) event->y, (int) event->x, (int) event->y, redraw_boxes))
566     {
567         for (auto& pos : redraw_boxes)
568         {
569             debug_print("x: %d y:%d w: %d h: %d\n", pos.x, pos.y, pos.width, pos.height);
570             gtk_widget_queue_draw_area(widget, pos.x, pos.y, pos.width, pos.height);
571         }
572     }
573 
574     if (!w->m_clicked_url.empty())
575     {
576             debug_print("Open in browser: %s\n", w->m_clicked_url.c_str());
577             open_uri(w->m_clicked_url.c_str(), prefs_common_get_uri_cmd());
578     }
579 
580 	return true;
581 }
582 
open_link_cb(GtkMenuItem * item,gpointer user_data)583 static void open_link_cb(GtkMenuItem *item, gpointer user_data)
584 {
585 	lh_widget_wrapped *w = (lh_widget_wrapped *)user_data;
586 
587 	open_uri(w->m_clicked_url.c_str(), prefs_common_get_uri_cmd());
588 }
589 
copy_link_cb(GtkMenuItem * item,gpointer user_data)590 static void copy_link_cb(GtkMenuItem *item, gpointer user_data)
591 {
592 	lh_widget_wrapped *w = (lh_widget_wrapped *)user_data;
593 
594 	gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_PRIMARY),
595 			w->m_clicked_url.c_str(), -1);
596 	gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD),
597 			w->m_clicked_url.c_str(), -1);
598 }
599 
600 ///////////////////////////////////////////////////////////
601 extern "C" {
602 
lh_widget_new()603 lh_widget_wrapped *lh_widget_new()
604 {
605 	return new lh_widget;
606 }
607 
lh_widget_get_widget(lh_widget_wrapped * w)608 GtkWidget *lh_widget_get_widget(lh_widget_wrapped *w)
609 {
610 	return w->get_widget();
611 }
612 
lh_widget_open_html(lh_widget_wrapped * w,const gchar * path)613 void lh_widget_open_html(lh_widget_wrapped *w, const gchar *path)
614 {
615 	w->open_html(path);
616 }
617 
lh_widget_clear(lh_widget_wrapped * w)618 void lh_widget_clear(lh_widget_wrapped *w)
619 {
620 	w->clear();
621 }
622 
lh_widget_destroy(lh_widget_wrapped * w)623 void lh_widget_destroy(lh_widget_wrapped *w)
624 {
625 	delete w;
626 }
627 
lh_widget_print(lh_widget_wrapped * w)628 void lh_widget_print(lh_widget_wrapped *w) {
629 	w->print();
630 }
631 
lh_widget_set_partinfo(lh_widget_wrapped * w,MimeInfo * partinfo)632 void lh_widget_set_partinfo(lh_widget_wrapped *w, MimeInfo *partinfo)
633 {
634 	w->set_partinfo(partinfo);
635 }
636 
637 } /* extern "C" */
638