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