1 /**
2  * @file update.c  generic update request and state processing
3  *
4  * Copyright (C) 2003-2014 Lars Windolf <lars.windolf@gmx.de>
5  * Copyright (C) 2004-2006 Nathan J. Conrad <t98502@users.sourceforge.net>
6  * Copyright (C) 2009 Adrian Bunk <bunk@users.sourceforge.net>
7  *
8  * This program is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation; either version 2 of the License, or
11  * (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with this program; if not, write to the Free Software
20  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
21  */
22 
23 #include "update.h"
24 
25 #include <libxml/parser.h>
26 #include <libxslt/xslt.h>
27 #include <libxslt/xsltInternals.h>
28 #include <libxslt/transform.h>
29 #include <libxslt/xsltutils.h>
30 
31 #include <libpeas/peas-extension-set.h>
32 
33 #include <unistd.h>
34 #include <stdio.h>
35 #if !defined (G_OS_WIN32) || defined (HAVE_SYS_WAIT_H)
36 #include <sys/wait.h>
37 #endif
38 #include <string.h>
39 
40 #include "auth_activatable.h"
41 #include "common.h"
42 #include "debug.h"
43 #include "net.h"
44 #include "plugins_engine.h"
45 #include "xml.h"
46 #include "ui/liferea_shell.h"
47 
48 #if defined (G_OS_WIN32) && !defined (WIFEXITED) && !defined (WEXITSTATUS)
49 #define WIFEXITED(x) (x != 0)
50 #define WEXITSTATUS(x) (x)
51 #endif
52 
53 /** global update job list, used for lookups when cancelling */
54 static GSList	*jobs = NULL;
55 
56 static GAsyncQueue *pendingHighPrioJobs = NULL;
57 static GAsyncQueue *pendingJobs = NULL;
58 static guint numberOfActiveJobs = 0;
59 #define MAX_ACTIVE_JOBS	5
60 
61 /* update state interface */
62 
63 updateStatePtr
update_state_new(void)64 update_state_new (void)
65 {
66 	return g_new0 (struct updateState, 1);
67 }
68 
69 glong
update_state_get_lastmodified(updateStatePtr state)70 update_state_get_lastmodified (updateStatePtr state)
71 {
72 	return state->lastModified;
73 }
74 
75 void
update_state_set_lastmodified(updateStatePtr state,glong lastModified)76 update_state_set_lastmodified (updateStatePtr state, glong lastModified)
77 {
78 	state->lastModified = lastModified;
79 }
80 
81 const gchar *
update_state_get_etag(updateStatePtr state)82 update_state_get_etag (updateStatePtr state)
83 {
84 	return state->etag;
85 }
86 
87 void
update_state_set_etag(updateStatePtr state,const gchar * etag)88 update_state_set_etag (updateStatePtr state, const gchar *etag)
89 {
90 	g_free (state->etag);
91 	state->etag = NULL;
92 	if (etag)
93 		state->etag = g_strdup(etag);
94 }
95 
96 void
update_state_set_cache_maxage(updateStatePtr state,const gint maxage)97 update_state_set_cache_maxage (updateStatePtr state, const gint maxage)
98 {
99 	if (0 < maxage)
100 		state->maxAgeMinutes = maxage;
101 	else
102 		state->maxAgeMinutes = -1;
103 }
104 
105 gint
update_state_get_cache_maxage(updateStatePtr state)106 update_state_get_cache_maxage (updateStatePtr state)
107 {
108 	return state->maxAgeMinutes;
109 }
110 
111 const gchar *
update_state_get_cookies(updateStatePtr state)112 update_state_get_cookies (updateStatePtr state)
113 {
114 	return state->cookies;
115 }
116 
117 void
update_state_set_cookies(updateStatePtr state,const gchar * cookies)118 update_state_set_cookies (updateStatePtr state, const gchar *cookies)
119 {
120 	g_free (state->cookies);
121 	state->cookies = NULL;
122 	if (cookies)
123 		state->cookies = g_strdup (cookies);
124 }
125 
126 updateStatePtr
update_state_copy(updateStatePtr state)127 update_state_copy (updateStatePtr state)
128 {
129 	updateStatePtr newState;
130 
131 	newState = update_state_new ();
132 	update_state_set_lastmodified (newState, update_state_get_lastmodified (state));
133 	update_state_set_cookies (newState, update_state_get_cookies (state));
134 	update_state_set_etag (newState, update_state_get_etag (state));
135 
136 	return newState;
137 }
138 
139 void
update_state_free(updateStatePtr updateState)140 update_state_free (updateStatePtr updateState)
141 {
142 	if (!updateState)
143 		return;
144 
145 	g_free (updateState->cookies);
146 	g_free (updateState->etag);
147 	g_free (updateState);
148 }
149 
150 /* update request processing */
151 
152 updateRequestPtr
update_request_new(void)153 update_request_new (void)
154 {
155 	return g_new0 (struct updateRequest, 1);
156 }
157 
158 void
update_request_free(updateRequestPtr request)159 update_request_free (updateRequestPtr request)
160 {
161 	if (!request)
162 		return;
163 
164 	update_state_free (request->updateState);
165 	update_options_free (request->options);
166 
167 	g_free (request->postdata);
168 	g_free (request->source);
169 	g_free (request->filtercmd);
170 	g_free (request);
171 }
172 
173 void
update_request_set_source(updateRequestPtr request,const gchar * source)174 update_request_set_source(updateRequestPtr request, const gchar* source)
175 {
176 	g_free (request->source);
177 	request->source = g_strdup(source) ;
178 }
179 
180 void
update_request_set_auth_value(updateRequestPtr request,const gchar * authValue)181 update_request_set_auth_value(updateRequestPtr request, const gchar* authValue)
182 {
183 	g_free(request->authValue);
184 	request->authValue = g_strdup(authValue);
185 }
186 
187 updateResultPtr
update_result_new(void)188 update_result_new (void)
189 {
190 	updateResultPtr	result;
191 
192 	result = g_new0 (struct updateResult, 1);
193 	result->updateState = update_state_new ();
194 
195 	return result;
196 }
197 
198 void
update_result_free(updateResultPtr result)199 update_result_free (updateResultPtr result)
200 {
201 	if (!result)
202 		return;
203 
204 	update_state_free (result->updateState);
205 
206 	g_free (result->data);
207 	g_free (result->source);
208 	g_free (result->contentType);
209 	g_free (result->filterErrors);
210 	g_free (result);
211 }
212 
213 updateOptionsPtr
update_options_copy(updateOptionsPtr options)214 update_options_copy (updateOptionsPtr options)
215 {
216 	updateOptionsPtr newOptions;
217 
218 	newOptions = g_new0 (struct updateOptions, 1);
219 	newOptions->username = g_strdup (options->username);
220 	newOptions->password = g_strdup (options->password);
221 	newOptions->dontUseProxy = options->dontUseProxy;
222 
223 	return newOptions;
224 }
225 
226 void
update_options_free(updateOptionsPtr options)227 update_options_free (updateOptionsPtr options)
228 {
229 	if (!options)
230 		return;
231 
232 	g_free (options->username);
233 	g_free (options->password);
234 	g_free (options);
235 }
236 
237 /* update job handling */
238 
239 static updateJobPtr
update_job_new(gpointer owner,updateRequestPtr request,update_result_cb callback,gpointer user_data,updateFlags flags)240 update_job_new (gpointer owner,
241                 updateRequestPtr request,
242 		update_result_cb callback,
243 		gpointer user_data,
244 		updateFlags flags)
245 {
246 	updateJobPtr	job;
247 
248 	job = g_new0 (struct updateJob, 1);
249 	job->owner = owner;
250 	job->request = request;
251 	job->result = update_result_new ();
252 	job->callback = callback;
253 	job->user_data = user_data;
254 	job->flags = flags;
255 	job->state = REQUEST_STATE_INITIALIZED;
256 
257 	return job;
258 }
259 
260 gint
update_job_get_state(updateJobPtr job)261 update_job_get_state (updateJobPtr job)
262 {
263 	return job->state;
264 }
265 
266 static void
update_job_free(updateJobPtr job)267 update_job_free (updateJobPtr job)
268 {
269 	if (!job)
270 		return;
271 
272 	jobs = g_slist_remove (jobs, job);
273 
274 	update_request_free (job->request);
275 	update_result_free (job->result);
276 	g_free (job);
277 }
278 
279 /* filter idea (and some of the code) was taken from Snownews */
280 static gchar *
update_exec_filter_cmd(gchar * cmd,gchar * data,gchar ** errorOutput,size_t * size)281 update_exec_filter_cmd (gchar *cmd, gchar *data, gchar **errorOutput, size_t *size)
282 {
283 	int		fd, status;
284 	gchar		*command;
285 	const gchar	*tmpdir = g_get_tmp_dir();
286 	char		*tmpfilename;
287 	char		*out = NULL;
288 	FILE		*file, *p;
289 
290 	*errorOutput = NULL;
291 	tmpfilename = g_build_filename (tmpdir, "liferea-XXXXXX", NULL);
292 
293 	fd = g_mkstemp(tmpfilename);
294 
295 	if(fd == -1) {
296 		debug1(DEBUG_UPDATE, "Error opening temp file %s to use for filtering!", tmpfilename);
297 		*errorOutput = g_strdup_printf(_("Error opening temp file %s to use for filtering!"), tmpfilename);
298 		g_free(tmpfilename);
299 		return NULL;
300 	}
301 
302 	file = fdopen(fd, "w");
303 	fwrite(data, strlen(data), 1, file);
304 	fclose(file);
305 
306 	*size = 0;
307 	command = g_strdup_printf("%s < %s", cmd, tmpfilename);
308 	p = popen(command, "r");
309 	g_free(command);
310 	if(NULL != p) {
311 		while(!feof(p) && !ferror(p)) {
312 			size_t len;
313 			out = g_realloc(out, *size+1025);
314 			len = fread(&out[*size], 1, 1024, p);
315 			if(len > 0)
316 				*size += len;
317 		}
318 		status = pclose(p);
319 		if(!(WIFEXITED(status) && WEXITSTATUS(status) == 0)) {
320 			*errorOutput = g_strdup_printf(_("%s exited with status %d"),
321 			                              cmd, WEXITSTATUS(status));
322 			*size = 0;
323 		}
324 		out[*size] = '\0';
325 	} else {
326 		g_warning(_("Error: Could not open pipe \"%s\""), command);
327 		*errorOutput = g_strdup_printf(_("Error: Could not open pipe \"%s\""), command);
328 	}
329 	/* Clean up. */
330 	unlink (tmpfilename);
331 	g_free (tmpfilename);
332 	return out;
333 }
334 
335 static gchar *
update_apply_xslt(updateJobPtr job)336 update_apply_xslt (updateJobPtr job)
337 {
338 	xsltStylesheetPtr	xslt = NULL;
339 	xmlOutputBufferPtr	buf;
340 	xmlDocPtr		srcDoc = NULL, resDoc = NULL;
341 	gchar			*output = NULL;
342 
343 	g_assert (NULL != job->result);
344 
345 	do {
346 		srcDoc = xml_parse (job->result->data, job->result->size, NULL);
347 		if (!srcDoc) {
348 			g_warning("fatal: parsing request result XML source failed (%s)!", job->request->filtercmd);
349 			break;
350 		}
351 
352 		/* load localization stylesheet */
353 		xslt = xsltParseStylesheetFile (job->request->filtercmd);
354 		if (!xslt) {
355 			g_warning ("fatal: could not load filter stylesheet \"%s\"!", job->request->filtercmd);
356 			break;
357 		}
358 
359 		resDoc = xsltApplyStylesheet (xslt, srcDoc, NULL);
360 		if (!resDoc) {
361 			g_warning ("fatal: applying stylesheet \"%s\" failed!", job->request->filtercmd);
362 			break;
363 		}
364 
365 		buf = xmlAllocOutputBuffer (NULL);
366 		if (-1 == xsltSaveResultTo (buf, resDoc, xslt)) {
367 			g_warning ("fatal: retrieving result of filter stylesheet failed (%s)!", job->request->filtercmd);
368 			break;
369 		}
370 
371 #ifdef LIBXML2_NEW_BUFFER
372 		if (xmlOutputBufferGetSize (buf) > 0)
373 			output = xmlCharStrdup (xmlOutputBufferGetContent (buf));
374 #else
375 		if (xmlBufferLength (buf->buffer) > 0)
376 			output = xmlCharStrdup (xmlBufferContent (buf->buffer));
377 #endif
378 
379 		xmlOutputBufferClose (buf);
380 	} while (FALSE);
381 
382 	if (srcDoc)
383 		xmlFreeDoc (srcDoc);
384 	if (resDoc)
385 		xmlFreeDoc (resDoc);
386 	if (xslt)
387 		xsltFreeStylesheet (xslt);
388 
389 	return output;
390 }
391 
392 static void
update_apply_filter(updateJobPtr job)393 update_apply_filter (updateJobPtr job)
394 {
395 	gchar	*filterResult;
396 	size_t	len = 0;
397 
398 	g_assert (NULL == job->result->filterErrors);
399 
400 	/* we allow two types of filters: XSLT stylesheets and arbitrary commands */
401 	if ((strlen (job->request->filtercmd) > 4) &&
402 	    (0 == strcmp (".xsl", job->request->filtercmd + strlen (job->request->filtercmd) - 4))) {
403 		filterResult = update_apply_xslt (job);
404 		len = strlen (filterResult);
405 	} else {
406 		filterResult = update_exec_filter_cmd (job->request->filtercmd, job->result->data, &(job->result->filterErrors), &len);
407 	}
408 
409 	if (filterResult) {
410 		g_free (job->result->data);
411 		job->result->data = filterResult;
412 		job->result->size = len;
413 	}
414 }
415 
416 static void
update_exec_cmd(updateJobPtr job)417 update_exec_cmd (updateJobPtr job)
418 {
419 	FILE	*f;
420 	int	status;
421 	size_t	len;
422 
423 	job->result = update_result_new ();
424 
425 	/* if the first char is a | we have a pipe else a file */
426 	debug1 (DEBUG_UPDATE, "executing command \"%s\"...", (job->request->source) + 1);
427 	f = popen ((job->request->source) + 1, "r");
428 	if (f) {
429 		while (!feof (f) && !ferror (f)) {
430 			job->result->data = g_realloc (job->result->data, job->result->size + 1025);
431 			len = fread (&job->result->data[job->result->size], 1, 1024, f);
432 			if (len > 0)
433 				job->result->size += len;
434 		}
435 		status = pclose (f);
436 		if (WIFEXITED (status) && WEXITSTATUS (status) == 0)
437 			job->result->httpstatus = 200;
438 		else
439 			job->result->httpstatus = 404;	/* FIXME: maybe setting request->returncode would be better */
440 
441 		if (job->result->data)
442 			job->result->data[job->result->size] = '\0';
443 	} else {
444 		liferea_shell_set_status_bar (_("Error: Could not open pipe \"%s\""), (job->request->source) + 1);
445 		job->result->httpstatus = 404;	/* FIXME: maybe setting request->returncode would be better */
446 	}
447 
448 	update_process_finished_job (job);
449 }
450 
451 static void
update_load_file(updateJobPtr job)452 update_load_file (updateJobPtr job)
453 {
454 	gchar *filename = job->request->source;
455 	gchar *anchor;
456 
457 	job->result = update_result_new ();
458 
459 	if (!strncmp (filename, "file://",7))
460 		filename += 7;
461 
462 	anchor = strchr (filename, '#');
463 	if (anchor)
464 		*anchor = 0;	 /* strip anchors from filenames */
465 
466 	if (g_file_test (filename, G_FILE_TEST_EXISTS)) {
467 		/* we have a file... */
468 		if ((!g_file_get_contents (filename, &(job->result->data), &(job->result->size), NULL)) || (job->result->data[0] == '\0')) {
469 			job->result->httpstatus = 403;	/* FIXME: maybe setting request->returncode would be better */
470 			liferea_shell_set_status_bar (_("Error: Could not open file \"%s\""), filename);
471 		} else {
472 			job->result->httpstatus = 200;
473 			debug2 (DEBUG_UPDATE, "Successfully read %d bytes from file %s.", job->result->size, filename);
474 		}
475 	} else {
476 		liferea_shell_set_status_bar (_("Error: There is no file \"%s\""), filename);
477 		job->result->httpstatus = 404;	/* FIXME: maybe setting request->returncode would be better */
478 	}
479 
480 	update_process_finished_job (job);
481 }
482 
483 static void
update_job_run(updateJobPtr job)484 update_job_run (updateJobPtr job)
485 {
486 	/* Here we decide on the source type and the proper execution
487 	   methods which then do anything they want with the job and
488 	   pass the processed job to update_process_finished_job()
489 	   for result dequeuing */
490 
491 	/* everything starting with '|' is a local command */
492 	if (*(job->request->source) == '|') {
493 		debug1 (DEBUG_UPDATE, "Recognized local command: %s", job->request->source);
494 		update_exec_cmd (job);
495 		return;
496 	}
497 
498 	/* if it has a protocol "://" prefix, but not "file://" it is an URI */
499 	if (strstr (job->request->source, "://") && strncmp (job->request->source, "file://", 7)) {
500 		network_process_request (job);
501 		return;
502 	}
503 
504 	/* otherwise it must be a local file... */
505 	{
506 		debug1 (DEBUG_UPDATE, "Recognized file URI: %s", job->request->source);
507 		update_load_file (job);
508 		return;
509 	}
510 }
511 
512 static gboolean
update_dequeue_job(gpointer user_data)513 update_dequeue_job (gpointer user_data)
514 {
515 	updateJobPtr job;
516 
517 	if (!pendingJobs)
518 		return FALSE;	/* we must be in shutdown */
519 
520 	if (numberOfActiveJobs >= MAX_ACTIVE_JOBS)
521 		return FALSE;	/* we'll be called again when a job finishes */
522 
523 
524 	job = (updateJobPtr)g_async_queue_try_pop(pendingHighPrioJobs);
525 
526 	if (!job)
527 		job = (updateJobPtr)g_async_queue_try_pop(pendingJobs);
528 
529 	if(!job)
530 		return FALSE;	/* no request at the moment */
531 
532 	numberOfActiveJobs++;
533 
534 	job->state = REQUEST_STATE_PROCESSING;
535 
536 	debug1 (DEBUG_UPDATE, "processing request (%s)", job->request->source);
537 	if (job->callback == NULL) {
538 		update_process_finished_job (job);
539 	} else {
540 		update_job_run (job);
541 	}
542 
543 	return FALSE;
544 }
545 
546 updateJobPtr
update_execute_request(gpointer owner,updateRequestPtr request,update_result_cb callback,gpointer user_data,updateFlags flags)547 update_execute_request (gpointer owner,
548                         updateRequestPtr request,
549 			update_result_cb callback,
550 			gpointer user_data,
551 			updateFlags flags)
552 {
553 	updateJobPtr job;
554 
555 	g_assert (request->options != NULL);
556 	g_assert (request->source != NULL);
557 
558 	job = update_job_new (owner, request, callback, user_data, flags);
559 	job->state = REQUEST_STATE_PENDING;
560 	jobs = g_slist_append (jobs, job);
561 
562 	if (flags & FEED_REQ_PRIORITY_HIGH) {
563 		g_async_queue_push (pendingHighPrioJobs, (gpointer)job);
564 	} else {
565 		g_async_queue_push (pendingJobs, (gpointer)job);
566 	}
567 
568 	g_idle_add (update_dequeue_job, NULL);
569 	return job;
570 }
571 
572 void
update_job_cancel_by_owner(gpointer owner)573 update_job_cancel_by_owner (gpointer owner)
574 {
575 	GSList	*iter = jobs;
576 
577 	while (iter) {
578 		updateJobPtr job = (updateJobPtr)iter->data;
579 		if (job->owner == owner)
580 			job->callback = NULL;
581 		iter = g_slist_next (iter);
582 	}
583 }
584 
585 static gboolean
update_process_result_idle_cb(gpointer user_data)586 update_process_result_idle_cb (gpointer user_data)
587 {
588 	updateJobPtr job = (updateJobPtr)user_data;
589 
590 	if (job->callback)
591 		(job->callback) (job->result, job->user_data, job->flags);
592 
593 	update_job_free (job);
594 
595 	return FALSE;
596 }
597 
598 static void
update_apply_filter_async(GTask * task,gpointer src,gpointer tdata,GCancellable * ccan)599 update_apply_filter_async(GTask *task, gpointer src, gpointer tdata, GCancellable *ccan)
600 {
601     updateJobPtr job = tdata;
602     update_apply_filter(job);
603     g_task_return_int(task, 0);
604 }
605 
606 static void
update_apply_filter_finish(GObject * src,GAsyncResult * result,gpointer user_data)607 update_apply_filter_finish(GObject *src, GAsyncResult *result, gpointer user_data)
608 {
609     updateJobPtr job = user_data;
610     g_idle_add(update_process_result_idle_cb, job);
611 }
612 
613 void
update_process_finished_job(updateJobPtr job)614 update_process_finished_job (updateJobPtr job)
615 {
616 	job->state = REQUEST_STATE_DEQUEUE;
617 
618 	g_assert(numberOfActiveJobs > 0);
619 	numberOfActiveJobs--;
620 	g_idle_add (update_dequeue_job, NULL);
621 
622 	/* Handling abandoned requests (e.g. after feed deletion) */
623 	if (job->callback == NULL) {
624 		debug1 (DEBUG_UPDATE, "freeing cancelled request (%s)", job->request->source);
625 		update_job_free (job);
626 		return;
627 	}
628 
629 	/* Finally execute the postfilter */
630 	if (job->result->data && job->request->filtercmd) {
631                 GTask *task = g_task_new(NULL, NULL, update_apply_filter_finish, job);
632                 g_task_set_task_data(task, job, NULL);
633                 g_task_run_in_thread(task, update_apply_filter_async);
634                 g_object_unref(task);
635                 return;
636         }
637 
638 	g_idle_add (update_process_result_idle_cb, job);
639 }
640 
641 
642 void
update_init(void)643 update_init (void)
644 {
645 	pendingJobs = g_async_queue_new ();
646 	pendingHighPrioJobs = g_async_queue_new ();
647 }
648 
649 void
update_deinit(void)650 update_deinit (void)
651 {
652 	GSList	*iter = jobs;
653 
654 	/* Cancel all jobs, to avoid async callbacks accessing the GUI */
655 	while (iter) {
656 		updateJobPtr job = (updateJobPtr)iter->data;
657 		job->callback = NULL;
658 		iter = g_slist_next (iter);
659 	}
660 
661 	g_async_queue_unref (pendingJobs);
662 	g_async_queue_unref (pendingHighPrioJobs);
663 
664 	g_slist_free (jobs);
665 	jobs = NULL;
666 }
667