summaryrefslogtreecommitdiffstats
path: root/src/api.c
blob: b0ef96d43f2d69d2908366a875223b7f1d735e28 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
#define SC_CAPI(c, b, h, e, ...) sc_api(c, b, h, 0##__VA_OPT__(1), e __VA_OPT__(,) __VA_ARGS__)
#define SC_CAPIX(c, b, h, e, ...) sc_capix(c, b, h, 0##__VA_OPT__(1), e __VA_OPT__(,) __VA_ARGS__)
char * sc_api (struct sc_cache * c, char * body, char * headers, int isfmt, char * endpoint, ...) {
	if (!c || !endpoint)
		return NULL;
	size_t va_count = parse_printf_format(endpoint, 0, NULL);
	char * endpoint_formatted = NULL;
	long response_code = 0;
	if (isfmt && va_count > 0 && endpoint_formatted == NULL) {
		va_list ap, ap2;
		va_start(ap, endpoint);
		va_copy(ap2, ap);
		size_t strlenm = vsnprintf(NULL, 0, endpoint, ap);
		endpoint_formatted = malloc(sizeof(char)*strlenm+1);
		vsnprintf(endpoint_formatted, strlenm+1, endpoint, ap2);
		va_end(ap);
		va_end(ap2);
	}
	if (!headers)
		headers = "";
	char * hedrs = malloc(sizeof(char)*strlen(headers)+strlen(SC_HTTP_HEADERS)+1);
	strcpy(hedrs, SC_HTTP_HEADERS);
	strcat(hedrs, headers);
	char * contentType = NULL;
	char * redir = NULL;
	char * buf = malloc(sizeof(char)*SC_HTTP_RBUFSIZE);
	size_t buf_sizeof = SC_HTTP_RBUFSIZE;
	size_t buf_length = 0;
	int readstatus = 0;
	void * r = xmlNanoHTTPMethodRedir(
			endpoint_formatted ? endpoint_formatted : endpoint,
			body ? "POST" : "GET",
			body,
			&contentType,
			&redir,
			hedrs,
			body ? strlen(body) : 0
			);
	if (!r) {
		SC_LOG(SC_LOG_ERROR, c, "!r, endpoint: %s", endpoint_formatted ? endpoint_formatted : endpoint);
		goto rc;
	}
	response_code = xmlNanoHTTPReturnCode(r);
	if (!(response_code - 200 >= 0 && response_code - 200 < 100)) {
		SC_LOG(SC_LOG_ERROR, c, "response_code == %ld, endpoint: %s", response_code, endpoint_formatted ? endpoint_formatted:endpoint);
	}
	while ((readstatus = xmlNanoHTTPRead(r, buf+buf_length, buf_sizeof-buf_length)) > 0) {
		buf_length += readstatus;
		if (buf_sizeof-buf_length < SC_HTTP_RBUFSIZE) {
			buf_sizeof *= SC_REALLOC_K;
			buf = realloc(buf, sizeof(char)*buf_sizeof); /* this IS safe, no matter how hard valgrind complains */
		}
	}
	buf[buf_length++] = '\0';
	if (readstatus == -1)
		SC_LOG(SC_LOG_ERROR, c, "readstatus == -1, endpoint: %s", endpoint_formatted ? endpoint_formatted : endpoint);
	xmlNanoHTTPClose(r);
	SC_LOG(SC_LOG_DEBUG, c, "contentType = %s, redir = %s", contentType ? contentType : "NULL", redir ? redir : "NULL");
rc:
	free(endpoint_formatted);
	free(contentType);
	free(redir);
	free(hedrs);
	return buf;
}
htmlDocPtr sc_capix (struct sc_cache * c, char * body, char * headers, int isfmt, char * endpoint, ...) {
	if (!c || !endpoint)
		return NULL;
	size_t va_count = parse_printf_format(endpoint, 0, NULL);
	char * endpoint_formatted = NULL;
	if (isfmt && va_count > 0 && endpoint_formatted == NULL) {
		va_list ap, ap2;
		va_start(ap, endpoint);
		va_copy(ap2, ap);
		size_t strlenm = vsnprintf(NULL, 0, endpoint, ap);
		endpoint_formatted = malloc(sizeof(char)*strlenm+1);
		vsnprintf(endpoint_formatted, strlenm+1, endpoint, ap2);
		va_end(ap);
		va_end(ap2);
	}
	char * buf = sc_api(c, body, headers, 0, endpoint_formatted ? endpoint_formatted : endpoint);
	htmlDocPtr htmldoc = parseHtmlDocument(buf, endpoint_formatted ? endpoint_formatted : endpoint);
	free(buf);
	free(endpoint_formatted);
	return htmldoc;
}
char * sc_find_class (char * haystack, const char * definition) { /* you must free class after calling */
	if (!haystack || !definition)
		return NULL;
	char * class = strcasestr(haystack, definition);
	if (!class)
		return NULL;
	int found = 0;
	for (; class > haystack; class--)
		if (class[-1] == '.' && (found = 1))
			break;
	if (!found)
		return NULL;
	char * endofclass = class;
	found = 0;
	for (; *endofclass; endofclass++) /* google only has alphanumeric class names. TODO: be pedantic and conformic to w3 stds */
		if (!isalnum(endofclass[0]) && (found = 1))
			break;
	if (!found)
		return NULL;
	char * toreturn = malloc(endofclass-class+1);
	strncpy(toreturn, class, endofclass-class);
	toreturn[endofclass-class] = '\0';
	return toreturn;
}
int sc_fix_url (char ** h) { /* fixes a (result) URL in-place (removes tracking nonsense, so resulting URL is shorter or equl) */
	if (!h || !*h) /* stage 0: prevent accidental death */
		return -1;
	if (!strncmp(*h, "/url?q=", strlen("/url?q="))) { /* stage 1: url may be tracking url by google results */
		*h = *h+strlen("/url?q=");
		*strchrnul(*h, '&') = '\0';
		urldecode(*h, *h);
	}
	char * c = NULL;
	if ((c = strcasestr(*h, "googleweblight.com/fp?u="))) { /* stage 2: url may be "light web" tracking url by google results */
		*h = c+strlen("googleweblight.com/fp?u="); /* we could disable this with a cookie but meh, this is easier and _stateless_ */
		*strchrnul(*h, '&') = '\0';
		urldecode(*h, *h);
	} /* TODO: be pedantic and remove utm_source and other tracking bullshit */
	return 1;
}
enum sc_return sc_query_google (const char * s, /* breaking change: changed return type */
		struct sc_cache * c,
		struct sc_query * q,
		char ** redirect, /* variable redirect will be set to a heap allocated string that must be freed by the caller if the upstream returned results for a different query. in that case the returned query object will be for a different search string! -- if NULL, request that upstream does not enable "results for" feature */
		SC_OPT_TYPE opt) { /* check4cachedB4 */
	/* query is in most cases NULL. then it will be allocated and put into sc_cache. otherwise response will be put into passed q. */
	/* if query is not NULL, it MUST be initialized */
	/*
		remarks:
			* we are using wap.google.com over HTTP and with a user-agent string of a nokia mobile phone, so we get a lite website
			* we determine which class holds a specific value by looking at the css definitions
				- result title: the only class that has definition {color:#1967D2;font-size:14px;line-height:16px}
					+ A links have this class set, but they have a child SPAN element that then holds the text of the title
					+ A href points to a tracking relative link, starting with /url?q=. the q parameter contains the (obv urlencoded) link.
				- result date: class has only one definition, {color:#70757a}, but same definition has the class for the settings A link.
					+ extract those two classes and find the one that is only present on SPAN text elements.
				- result description: once we have the result div, the description is the //table//span with the appropriate class
					+ the appropriate class is the only one with {word-break:break-word}. note that this class also describes other elements.
				- result div: to get the result div, we need the parent of the parent of the A link of the title.
			* result dates are sometimes relative ("an hour ago") and heavily depend on the client location, based on IP.
				- we won't parse those yet
			* I couldn't find anything with ratings, so we won't parse thouse either yet
			* captcha: google knows that this nokia phone we're pretending to be doesn't support javascript, but does not care, and loads an obfuscated captcha anyways that would be hard to defeat for now without some kind of chromium emulation we really don't want.
	*/
	enum sc_return rs = 1;
	char * xpath = NULL;
	char * descclass = NULL;
	char * titleclass = NULL;
	char * imageclass = NULL;
	htmlDocPtr xmldoc = NULL;
	char * txtdoc = NULL;
	int qwasgiven = 0;
	SC_LOG(SC_LOG_DEBUG, c, "%s called, redirect is %p", __func__, redirect);
	if (redirect)
		*redirect = NULL;
	if (!s || !c) {
		rs = SC_BADCALL;
		goto rc;
	}
	int sl = strlen(s);
	if (!q)
		q = sc_query_init();
	else
		qwasgiven++;
	char * us = malloc(sizeof(char)*strlen(s)*3+1);
	urlencode(us, s);
	txtdoc = SC_CAPI(c, NULL, NULL, "http://wap.google.com/search?q=%s&num=100&ie=UTF-8%s%s", us, (opt&SC_OPT_IMAGE) ? "&tbm=isch" : "", redirect ? "" : "&nfpr=1");
	// fprintf(stdout, "%s\n", txtdoc);
	free(us);
	if (!txtdoc) {
		SC_LOG(SC_LOG_ERROR, c, "!txtdoc");
		rs = SC_EMPTYRESPONSE;
		goto rc;
	}
	if (strstr(txtdoc, "In the meantime, solving the above CAPTCHA will let you continue")) {
		rs = SC_CAPTCHA;
		goto rc;
	}
	char * resultsforclass = sc_find_class(txtdoc, "{color:#1967d2}");
	if (opt & SC_OPT_IMAGE) {
		imageclass = sc_find_class(txtdoc, "{font-family:Roboto,Helvetica,Arial,sans-serif}");
		if (!imageclass) {
			SC_LOG(SC_LOG_ERROR, c, "!imageclass, txtdoc = %s", txtdoc);
			rs = SC_NOIMGCLASS;
			goto rc;
		}
	} else {
		titleclass = sc_find_class(txtdoc, "{color:#1967D2;font-size:14px;line-height:16px}");
		descclass = sc_find_class(txtdoc, "{word-break:break-word}");
		if (!titleclass || !descclass) {
			SC_LOG(SC_LOG_ERROR, c, "!titleclass || !descclass, txtdoc = %s", txtdoc);
			rs = SC_NOCLASS;
			goto rc;
		}
	}
#define SC_GTXF "/html/body//a[contains(@class, '%s')]" /* result a */
#define SC_GTXD /* description */ "../..//table//span[@class='%s']"
#define SC_GTXB /* breadcrumbs */ ".//span[@class='%s']"
#define SC_GTXI "//div[@class='%s']//a"
#define SC_GTR q->results[q->results_length-1]
	xpath = malloc(strlen((opt & SC_OPT_IMAGE) ? imageclass : titleclass)+strlen((opt & SC_OPT_IMAGE) ? SC_GTXI : SC_GTXF));
	sprintf(xpath, (opt & SC_OPT_IMAGE) ? SC_GTXI : SC_GTXF, (opt & SC_OPT_IMAGE) ? imageclass : titleclass);
	xmldoc = parseHtmlDocument(txtdoc, NULL);
	if (qwasgiven) /* as you can see, when q is given, queries will be write-locked for the whole XML processing time! */
		SC_CWLE(c, c->queries_lock);
	q->results_length = 0;
	gnu_code_start;
	void sc_query_google_eachnode (xmlNodePtr node, void * data) {
		if (node->type == XML_ELEMENT_NODE) {
			xmlAttrPtr href = xmlHasProp(node, BAD_CAST "href");
			if (href) {
				char * hreflink = (char *) xmlGetProp(node, BAD_CAST "href"); /* xmlGetProp copies and allocates */
				if (!hreflink) {
					SC_LOG(SC_LOG_ERROR, c, "!hreflink");
					rs = SC_NOHREF;
					return;
				}
				if (opt & SC_OPT_IMAGE) {
					char * imgurl = NULL; /* do not free those when allocated by sscanf, as they will directly go into the struct. */
					char * imgrefurl = NULL; /* easy, huh? */
					SC_LOG(SC_LOG_DEBUG, c, "hreflink = %s", hreflink);
					sscanf(hreflink, "/imgres?imgurl=%m[^&]&imgrefurl=%m[^&]", &imgurl, &imgrefurl);
					xmlFree(hreflink);
					if (!imgurl && !imgrefurl) {
						SC_LOG(SC_LOG_ERROR, c, "!imgurl && !imgrefurl, txtdoc = %s", txtdoc);
						/* rs = -7; */ /* we continue running not fail because of a single picture */
						free(imgurl);
						free(imgrefurl);
						return; /* check! */
					}
					urldecode(imgurl, imgurl);
					urldecode(imgrefurl, imgrefurl);
					if (q->results_sizeof <= q->results_length)
						SC_BIGGER_ARRAY(q->results, sc_result, 1);
					q->results_length++;
					SC_GTR->query = q;
					SC_GTR->title = NULL; /* can't get title from here, would have to load /imgres, which is bloat */
					SC_GTR->url = imgrefurl;
					SC_GTR->desc = imgurl;
					SC_GTR->breadcrumbs = NULL;
				} else {
					char * orig_hreflink_for_free = hreflink;
					sc_fix_url(&hreflink);
					char * x = malloc(strlen(descclass)+strlen(SC_GTXD));
					char * xbread = malloc(strlen(descclass)+strlen(SC_GTXB));
					sprintf(x, SC_GTXD, descclass /* remember, kids, GNU C is fucking legendary */);
					sprintf(xbread, SC_GTXB, descclass /* remember, kids, GNU C is fucking legendary */);
					xmlNodePtr descnode = nthNodeXN(node, x, 0);
					if (!descnode) /* description may be above, see https://support.google.com/websearch?p=featured_snippets */
						descnode = nthNodeXN(node, "../../div/div", 0);
					xmlNodePtr breadnode = nthNodeXN(node, xbread, 0);
					free(x);
					free(xbread);
					if (q->results_sizeof <= q->results_length)
						SC_BIGGER_ARRAY(q->results, sc_result, 1);
					q->results_length++;
					SC_GTR->query = q;
					char * cp = (char *) xmlNodeGetContent(node->children);
					if (cp) {
						SC_GTR->title = malloc(strlen(cp)+1);
						strcpy(SC_GTR->title, cp);
						xmlFree(cp);
					} else SC_GTR->title = NULL;
					if (hreflink) {
						SC_GTR->url = malloc(strlen(hreflink)+1);
						strcpy(SC_GTR->url, hreflink);
					} else SC_GTR->url = NULL;
					if (orig_hreflink_for_free)
						xmlFree(orig_hreflink_for_free);
					cp = (char *) xmlNodeGetContent(descnode);
					if (cp) {
						SC_GTR->desc = malloc(strlen(cp)+1);
						strcpy(SC_GTR->desc, cp);
						xmlFree(cp);
					} else SC_GTR->desc = NULL;
					cp = (char *) xmlNodeGetContent(breadnode);
					if (cp) {
						SC_GTR->breadcrumbs = malloc(strlen(cp)+1);
						strcpy(SC_GTR->breadcrumbs, cp);
						xmlFree(cp);
					}
				}
			}
		}
	}
	eachNodeX(xmldoc, xpath, sc_query_google_eachnode, NULL);
	gnu_code_end;
	if (rs < 0) {
		SC_LOG(SC_LOG_ERROR, c, "rs < 0 (rs == %d)", rs);
		if (qwasgiven)
			SC_CUE(c, c->queries_lock);
		goto rc;
	}
	q->string = realloc(q->string, sl+1);
	strcpy(q->string, s);
	char * xpathsugg = NULL;
	if (resultsforclass) {
		xpathsugg = malloc(512+strlen(resultsforclass));
		sprintf(xpathsugg, "//a[contains(@class, '%s')]", resultsforclass);
		xmlNodePtr suggnode = nthNodeX(xmldoc, xpathsugg, 0);
		if (suggnode && xmlHasProp(suggnode, BAD_CAST "href")) {
			char * href = (char *) xmlGetProp(suggnode, BAD_CAST "href");
			char * content = (char *) xmlNodeGetContent(suggnode);
			if (href && strstr(href, "&spell=1&"))
				strcpy((q->suggested = realloc(q->suggested, strlen(content)+1)), content);
			xmlFree(href);
			xmlFree(content);
		} else {
			free(q->suggested);
			q->suggested = NULL;
		}
	} else {
		free(q->suggested);
		q->suggested = NULL;
	}
	xmlNodePtr first = nthNodeX(xmldoc, xpathsugg, 1);
	if (redirect && xpathsugg && q->suggested && xmlHasProp(first, BAD_CAST "href")) {
		char * href = (char *) xmlGetProp(first, BAD_CAST "href");
		if (href && strstr(href, "&nfpr=1&")) {
			*redirect = q->suggested;
			q->suggested = NULL;
			q->string = realloc(q->string, strlen(*redirect)+1);
			strcpy(q->string, *redirect);
		}
		xmlFree(href);
	}
	q->cache = c;
	q->lookup_time = time(NULL);
	q->opt |= opt | SC_ENGINE_GOOGLE;
	if (!qwasgiven) {
		SC_CWLE(c, c->queries_lock);
#ifdef SC_OLD_STORAGE
		if (c->queries_sizeof <= c->queries_length)
			SC_BIGGER_ARRAY(c->queries, sc_query, 0);
		c->queries_length++;
#define SC_GTQ c->queries[c->queries_length-1]
		SC_GTQ = q;
#else /* we don't detect here if query is already stored, but it should not be ... */
		tsearch(q, &c->qrp, SC_COMPAR_CAST sc_query_compar);
#endif
	}
	SC_CUE(c, c->queries_lock);
rc:
	if (!qwasgiven && rs < 0)
		sc_query_free(q);
	xmlFreeDoc(xmldoc);
	free(txtdoc);
	free(titleclass);
	free(descclass);
	free(imageclass);
	free(resultsforclass);
	free(xpath);
	free(xpathsugg);
	return rs;
}