1 /**
2  * @file render.c  generic GTK theme and XSLT rendering handling
3  *
4  * Copyright (C) 2006-2018 Lars Windolf <lars.windolf@gmx.de>
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19  */
20 
21 #ifdef HAVE_CONFIG_H
22 #  include <config.h>
23 #endif
24 
25 #include <libxml/parser.h>
26 #include <libxml/tree.h>
27 #include <libxslt/xslt.h>
28 #include <libxslt/xsltInternals.h>
29 #include <libxslt/transform.h>
30 #include <libxslt/xsltutils.h>
31 #include <locale.h>
32 #include <math.h>
33 #include <string.h>
34 
35 #include "conf.h"
36 #include "common.h"
37 #include "debug.h"
38 #include "item.h"
39 #include "itemset.h"
40 #include "render.h"
41 #include "xml.h"
42 #include "ui/liferea_htmlview.h"
43 
44 /* Liferea provides special screens and the item and the feed displays
45    using self-generated HTML. To separate code and layout and to easily
46    localize the layout it is provided by automake XSL stylesheet templates.
47 
48    Using automake translations are merged into those XSL stylesheets. On
49    startup Liferea loads those expanded XSL stylesheets. During startup
50    Liferea initially reduces the contained translations to the currently
51    used ones by stripping all others from the XSL stylesheet using the
52    localization stylesheet (xslt/i18n-filter.xslt). The resulting XSLT
53    instance is kept in memory and used to render each items and feeds.
54 
55    The following code uses a hash table to maintain stylesheet instance
56    and performs CSS adaptions to the current GTK theme. */
57 
58 static renderParamPtr	langParams = NULL;	/* the current locale settings (for localization stylesheet) */
59 
60 static GHashTable	*stylesheets = NULL;	/* XSLT stylesheet cache */
61 
62 static void
render_parameter_free(renderParamPtr paramSet)63 render_parameter_free (renderParamPtr paramSet)
64 {
65 	g_strfreev (paramSet->params);
66 	g_free (paramSet);
67 }
68 
69 static void
render_init(void)70 render_init (void)
71 {
72 	gchar   	**shortlang = NULL;	/* e.g. "de" */
73 	gchar		**lang = NULL;		/* e.g. "de_AT" */
74 	gchar		*filename;
75 
76 	if (langParams)
77 		render_parameter_free (langParams);
78 
79 	/* Install default stylesheet if it does not yet exist */
80 	filename = common_create_config_filename ("liferea.css");
81 	if (!g_file_test (filename, G_FILE_TEST_EXISTS))
82 		common_copy_file (PACKAGE_DATA_DIR "/" PACKAGE "/css/user.css", filename);
83 	g_free(filename);
84 
85 	/* Prepare localization parameters */
86 	debug1 (DEBUG_HTML, "XSLT localisation: setlocale(LC_MESSAGES, NULL) reports '%s'", setlocale(LC_MESSAGES, NULL));
87 	lang = g_strsplit (setlocale (LC_MESSAGES, NULL), "@", 0);
88 	shortlang = g_strsplit (setlocale (LC_MESSAGES, NULL), "_", 0);
89 
90 	langParams = render_parameter_new ();
91 	render_parameter_add (langParams, "lang='%s'", lang[0]);
92 	render_parameter_add (langParams, "shortlang='%s'", shortlang[0]);
93 	debug2 (DEBUG_HTML, "XSLT localisation: lang='%s' shortlang='%s'", lang[0], shortlang[0]);
94 
95 	g_strfreev (shortlang);
96 	g_strfreev (lang);
97 
98 	if (!stylesheets)
99 		stylesheets = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
100 }
101 
102 static xsltStylesheetPtr
render_load_stylesheet(const gchar * xsltName)103 render_load_stylesheet (const gchar *xsltName)
104 {
105 	xsltStylesheetPtr	i18n_filter;
106 	xsltStylesheetPtr	xslt;
107 	xmlDocPtr		xsltDoc, resDoc;
108 	gchar			*filename;
109 
110 	if (!stylesheets)
111 		render_init ();
112 
113 	/* try to serve the stylesheet from the cache */
114 	xslt = (xsltStylesheetPtr)g_hash_table_lookup (stylesheets, xsltName);
115 	if (xslt)
116 		return xslt;
117 
118 	/* or load and translate it... */
119 
120 	/* 1. load localization stylesheet */
121 	i18n_filter = xsltParseStylesheetFile (PACKAGE_DATA_DIR G_DIR_SEPARATOR_S PACKAGE G_DIR_SEPARATOR_S "xslt" G_DIR_SEPARATOR_S "i18n-filter.xslt");
122 	if (!i18n_filter) {
123 		g_warning ("fatal: could not load localization stylesheet!");
124 		return NULL;
125 	}
126 
127 	/* 2. load and localize the rendering stylesheet */
128 	filename = g_strjoin (NULL, PACKAGE_DATA_DIR G_DIR_SEPARATOR_S PACKAGE G_DIR_SEPARATOR_S "xslt" G_DIR_SEPARATOR_S, xsltName, ".xml", NULL);
129 	xsltDoc = xmlParseFile (filename);
130 	if (!xsltDoc)
131 		g_warning ("fatal: could not load rendering stylesheet (%s)!", xsltName);
132 
133 	g_free (filename);
134 
135 	resDoc = xsltApplyStylesheet (i18n_filter, xsltDoc, (const gchar **)langParams->params);
136 	if (!resDoc)
137 		g_warning ("fatal: applying localization stylesheet failed (%s)!", xsltName);
138 
139 	/* Use the following to debug XSLT transformation problems */
140 	/* xsltSaveResultToFile (stdout, resDoc, i18n_filter); */
141 
142 	/* 3. create localized rendering stylesheet */
143 	xslt = xsltParseStylesheetDoc(resDoc);
144 	if (!xslt)
145 		g_warning("fatal: could not load rendering stylesheet (%s)!", xsltName);
146 
147 	xmlFreeDoc (xsltDoc);
148 	xsltFreeStylesheet (i18n_filter);
149 
150 	g_hash_table_insert (stylesheets, g_strdup (xsltName), xslt);
151 
152 	return xslt;
153 }
154 
155 /** cached CSS definitions */
156 static GString	*css = NULL;
157 
158 /** widget background theme colors as 8bit HTML RGB code */
159 typedef struct themeColor {
160 	const gchar	*name;
161 	gchar		*value;
162 } *themeColorPtr;
163 
164 static GSList *themeColors = NULL;
165 static gboolean darkTheme = FALSE;
166 
167 /* Determining of theme colors, to be inserted in CSS */
168 static themeColorPtr
render_calculate_theme_color(const gchar * name,GdkColor themeColor)169 render_calculate_theme_color (const gchar *name, GdkColor themeColor)
170 {
171 	themeColorPtr	tc;
172 	gushort		r, g, b;
173 
174 	r = themeColor.red / 256;
175 	g = themeColor.green / 256;
176 	b = themeColor.blue / 256;
177 
178 	tc = g_new0 (struct themeColor, 1);
179 	tc->name = name;
180 	tc->value = g_strdup_printf ("%.2X%.2X%.2X", r, g, b);
181 	debug2 (DEBUG_HTML, "theme color \"%s\" is %s", tc->name, tc->value);
182 
183 	return tc;
184 }
185 
186 static gint
render_get_rgb_distance(GdkColor * c1,GdkColor * c2)187 render_get_rgb_distance (GdkColor *c1, GdkColor *c2)
188 {
189 	return abs(
190 		(299 * c1->red/256 +
191 		 587 * c1->green/256 +
192 		 114 * c1->blue/256) -
193 		(299 * c2->red/256 +
194 		 587 * c2->green/256 +
195 		 114 * c2->blue/256)
196 	       ) / 1000;
197 }
198 
199 static void
rgba_to_color(GdkColor * color,GdkRGBA * rgba)200 rgba_to_color (GdkColor *color, GdkRGBA *rgba)
201 {
202 	color->red   = lrint (rgba->red   * 65535);
203 	color->green = lrint (rgba->green * 65535);
204 	color->blue  = lrint (rgba->blue  * 65535);
205 }
206 
207 void
render_init_theme_colors(GtkWidget * widget)208 render_init_theme_colors (GtkWidget *widget)
209 {
210 	GtkStyle	*style;
211 	GtkStyleContext	*sctxt;
212 	GdkColor	color;
213 	GdkRGBA		rgba;
214 	gint		textAvg, bgAvg;
215 
216 	style = gtk_widget_get_style (widget);
217 	sctxt = gtk_widget_get_style_context (widget);
218 
219 	g_assert (NULL == themeColors);
220 	themeColors = g_slist_append (themeColors, render_calculate_theme_color ("GTK-COLOR-FG",    style->fg[GTK_STATE_NORMAL]));
221 	themeColors = g_slist_append (themeColors, render_calculate_theme_color ("GTK-COLOR-BG",    style->bg[GTK_STATE_NORMAL]));
222 	themeColors = g_slist_append (themeColors, render_calculate_theme_color ("GTK-COLOR-LIGHT", style->light[GTK_STATE_NORMAL]));
223 	themeColors = g_slist_append (themeColors, render_calculate_theme_color ("GTK-COLOR-DARK",  style->dark[GTK_STATE_NORMAL]));
224 	themeColors = g_slist_append (themeColors, render_calculate_theme_color ("GTK-COLOR-MID",   style->mid[GTK_STATE_NORMAL]));
225 
226 	/* Sanity check text+base color as this causes many problems on dark
227 	   themes. If brightness distance is not enough we set text to fg/bg
228 	   which is always safe. */
229 	if (render_get_rgb_distance (&style->base[GTK_STATE_NORMAL], &style->text[GTK_STATE_NORMAL]) > 150) {
230 		// FIXME: Use theme labels instead of GTK-COLOR-<something> (e.g. CSS-BACKGROUND)
231 		themeColors = g_slist_append (themeColors, render_calculate_theme_color ("GTK-COLOR-BASE", style->base[GTK_STATE_NORMAL]));
232 		themeColors = g_slist_append (themeColors, render_calculate_theme_color ("GTK-COLOR-TEXT", style->text[GTK_STATE_NORMAL]));
233 	} else {
234 		themeColors = g_slist_append (themeColors, render_calculate_theme_color ("GTK-COLOR-BASE", style->bg[GTK_STATE_NORMAL]));
235 		themeColors = g_slist_append (themeColors, render_calculate_theme_color ("GTK-COLOR-TEXT", style->fg[GTK_STATE_NORMAL]));
236 	}
237 
238 	gtk_style_context_get_color (sctxt, GTK_STATE_FLAG_LINK, &rgba);
239 	rgba_to_color (&color, &rgba);
240 	themeColors = g_slist_append (themeColors, render_calculate_theme_color ("GTK-COLOR-NORMAL-LINK", color));
241 
242 	gtk_style_context_get_color (sctxt, GTK_STATE_FLAG_VISITED, &rgba);
243 	rgba_to_color (&color, &rgba);
244 	themeColors = g_slist_append (themeColors, render_calculate_theme_color ("GTK-COLOR-VISITED-LINK", color));
245 
246 	/* As there doesn't seem to be a safe way to determine wether we have a
247 	   dark GTK theme, let's guess it from the foreground vs. background
248 	   color average */
249 
250 	textAvg = style->text[GTK_STATE_NORMAL].red / 256 +
251 	        style->text[GTK_STATE_NORMAL].green / 256 +
252 	        style->text[GTK_STATE_NORMAL].blue / 256;
253 
254 	bgAvg = style->bg[GTK_STATE_NORMAL].red / 256 +
255 	        style->bg[GTK_STATE_NORMAL].green / 256 +
256 	        style->bg[GTK_STATE_NORMAL].blue / 256;
257 
258 	if (textAvg > bgAvg) {
259 		debug0 (DEBUG_HTML, "Dark GTK theme detected.");
260 		darkTheme = TRUE;
261 	}
262 
263 	if (darkTheme) {
264 		themeColors = g_slist_append (themeColors, render_calculate_theme_color ("FEEDLIST_UNREAD_BG", style->text[GTK_STATE_NORMAL]));
265 		/* Try nice foreground with 'fg' color (note: distance 50 is enough because it should be non-intrusive) */
266 		if (render_get_rgb_distance (&style->text[GTK_STATE_NORMAL], &style->fg[GTK_STATE_NORMAL]) > 50)
267 			themeColors = g_slist_append (themeColors, render_calculate_theme_color ("FEEDLIST_UNREAD_FG", style->fg[GTK_STATE_NORMAL]));
268 		else
269 			themeColors = g_slist_append (themeColors, render_calculate_theme_color ("FEEDLIST_UNREAD_FG", style->bg[GTK_STATE_NORMAL]));
270 	} else {
271 		themeColors = g_slist_append (themeColors, render_calculate_theme_color ("FEEDLIST_UNREAD_FG", style->bg[GTK_STATE_NORMAL]));
272 		/* Try nice foreground with 'dark' color (note: distance 50 is enough because it should be non-intrusive) */
273 		if (render_get_rgb_distance (&style->dark[GTK_STATE_NORMAL], &style->bg[GTK_STATE_NORMAL]) > 50)
274 			themeColors = g_slist_append (themeColors, render_calculate_theme_color ("FEEDLIST_UNREAD_BG", style->dark[GTK_STATE_NORMAL]));
275 		else
276 			themeColors = g_slist_append (themeColors, render_calculate_theme_color ("FEEDLIST_UNREAD_BG", style->fg[GTK_STATE_NORMAL]));
277 	}
278 }
279 
280 static gchar *
render_set_theme_colors(gchar * css)281 render_set_theme_colors (gchar *css)
282 {
283 	GSList	*iter = themeColors;
284 
285 	while (iter) {
286 		themeColorPtr tc = (themeColorPtr)iter->data;
287 		css = common_strreplace (css, tc->name, tc->value);
288 		iter = g_slist_next (iter);
289 	}
290 
291 	return css;
292 }
293 
294 const gchar *
render_get_theme_color(const gchar * name)295 render_get_theme_color (const gchar *name)
296 {
297 	GSList	*iter;
298 
299 	if (!themeColors)
300 		return NULL;
301 
302 	iter = themeColors;
303 	while (iter) {
304 		themeColorPtr tc = (themeColorPtr)iter->data;
305 		if (g_str_equal (name, tc->name))
306 			return tc->value;
307 		iter = g_slist_next (iter);
308 	}
309 
310 	return NULL;
311 }
312 
313 gboolean
render_is_dark_theme(void)314 render_is_dark_theme (void)
315 {
316 	if (!themeColors)
317 		return FALSE;
318 
319 	return darkTheme;
320 }
321 
322 const gchar *
render_get_css(gboolean externalCss)323 render_get_css (gboolean externalCss)
324 {
325 	if (!css) {
326 		gchar	*defaultStyleSheetFile;
327 		gchar	*userStyleSheetFile;
328 		gchar	*adblockStyleSheetFile;
329 		gchar	*tmp;
330 
331 		if (!themeColors)
332 			return NULL;
333 
334 		css = g_string_new(NULL);
335 
336 		defaultStyleSheetFile = g_build_filename (PACKAGE_DATA_DIR, PACKAGE, "css", "liferea.css", NULL);
337 
338 		if (g_file_get_contents(defaultStyleSheetFile, &tmp, NULL, NULL)) {
339 			tmp = render_set_theme_colors(tmp);
340 			g_string_append(css, tmp);
341 			g_free(tmp);
342 		} else {
343 			g_error ("Loading %s failed.", defaultStyleSheetFile);
344 		}
345 
346 		g_free(defaultStyleSheetFile);
347 
348 		userStyleSheetFile = common_create_config_filename ("liferea.css");
349 
350 		if (g_file_get_contents(userStyleSheetFile, &tmp, NULL, NULL)) {
351 			tmp = render_set_theme_colors(tmp);
352 			g_string_append(css, tmp);
353 			g_free(tmp);
354 		}
355 
356 		g_free(userStyleSheetFile);
357 
358 		adblockStyleSheetFile = g_build_filename(PACKAGE_DATA_DIR, PACKAGE, "css", "adblock.css", NULL);
359 
360 		if (g_file_get_contents(adblockStyleSheetFile, &tmp, NULL, NULL)) {
361 			g_string_append(css, tmp);
362 			g_free(tmp);
363 		}
364 
365 		g_free(adblockStyleSheetFile);
366 
367 		if (externalCss) {
368 			/* dump CSS to cache file and create a <style> tag to use it */
369 			gchar *filename = common_create_cache_filename (NULL, "style", "css");
370 			if (!g_file_set_contents(filename, css->str, -1, NULL))
371 				g_warning("Cannot write temporary CSS file \"%s\"!", filename);
372 
373 			g_string_free(css, TRUE);
374 
375 			css = g_string_new("<style type=\"text/css\"> @import url(file://");
376 			g_string_append(css, filename);
377 			g_string_append(css, "); </style> ");
378 
379 			g_free(filename);
380 		} else {
381 			/* keep the CSS in memory to serve it as a part of each HTML output */
382 			g_string_prepend(css, "<style type=\"text/css\">\n<![CDATA[\n");
383 			g_string_append(css, "\n]]>\n</style>\n");
384 		}
385 	}
386 
387 	return css->str;
388 }
389 
390 gchar *
render_xml(xmlDocPtr doc,const gchar * xsltName,renderParamPtr paramSet)391 render_xml (xmlDocPtr doc, const gchar *xsltName, renderParamPtr paramSet)
392 {
393 	gchar			*output = NULL;
394 	xmlDocPtr		resDoc;
395 	xsltStylesheetPtr	xslt;
396 	xmlOutputBufferPtr	buf;
397 
398 	xslt = render_load_stylesheet(xsltName);
399 	if (!xslt)
400 		return NULL;
401 
402 	if (!paramSet)
403 		paramSet = render_parameter_new ();
404 	render_parameter_add (paramSet, "pixmapsDir='file://" PACKAGE_DATA_DIR G_DIR_SEPARATOR_S PACKAGE G_DIR_SEPARATOR_S "pixmaps" G_DIR_SEPARATOR_S "'");
405 
406 	resDoc = xsltApplyStylesheet (xslt, doc, (const gchar **)paramSet->params);
407 	if (!resDoc) {
408 		g_warning ("fatal: applying rendering stylesheet (%s) failed!", xsltName);
409 		return NULL;
410 	}
411 
412 	/* for debugging use: xsltSaveResultToFile(stdout, resDoc, xslt); */
413 
414 	/* save results into return string */
415 	buf = xmlAllocOutputBuffer (NULL);
416 	if (-1 == xsltSaveResultTo(buf, resDoc, xslt))
417 		g_warning ("fatal: retrieving result of rendering stylesheet failed (%s)!", xsltName);
418 
419 #ifdef LIBXML2_NEW_BUFFER
420 	if (xmlOutputBufferGetSize (buf) > 0)
421 		output = xmlCharStrdup (xmlOutputBufferGetContent (buf));
422 #else
423 	if (xmlBufferLength (buf->buffer) > 0)
424 		output = xmlCharStrdup (xmlBufferContent(buf->buffer));
425 #endif
426 
427 	xmlOutputBufferClose (buf);
428 	xmlFreeDoc (resDoc);
429 	render_parameter_free (paramSet);
430 
431 	if (output) {
432 		gchar *tmp;
433 
434 		/* Return only the body contents */
435 		tmp = strstr (output, "<body");
436 		if (tmp) {
437 			tmp = g_strdup (tmp);
438 			xmlFree (output);
439 			output = tmp;
440 			tmp = strstr (output, "</body>");
441 			if (tmp) {
442 				tmp += 7;
443 				*tmp = 0;
444 			}
445 		}
446 	}
447 
448 	return output;
449 }
450 
451 /* parameter handling */
452 
453 renderParamPtr
render_parameter_new(void)454 render_parameter_new (void)
455 {
456 	return g_new0 (struct renderParam, 1);
457 }
458 
459 void
render_parameter_add(renderParamPtr paramSet,const gchar * fmt,...)460 render_parameter_add (renderParamPtr paramSet, const gchar *fmt, ...)
461 {
462 	gchar	*new, *value, *name;
463 	va_list args;
464 
465 	g_assert (NULL != fmt);
466 	g_assert (NULL != paramSet);
467 
468 	va_start (args, fmt);
469 	new = g_strdup_vprintf (fmt, args);
470 	va_end (args);
471 
472 	name = new;
473 	value = strchr (new, '=');
474 	g_assert (NULL != value);
475 	*value = 0;
476 	value++;
477 
478 	paramSet->len += 2;
479 	paramSet->params = (gchar **)g_realloc (paramSet->params, (paramSet->len + 1)*sizeof(gchar *));
480 	paramSet->params[paramSet->len] = NULL;
481 	paramSet->params[paramSet->len-2] = g_strdup (name);
482 	paramSet->params[paramSet->len-1] = g_strdup (value);
483 
484 	g_free (new);
485 }
486