Plugins/Purple Service/libpurple_extensions/fbapi.c
branchadium-1.5.11
changeset 6014 fcb71cb71a3d
parent 5941 307f53385811
parent 6013 f8d0dc659e3f
child 6016 325e2ab3406f
equal deleted inserted replaced
5941:307f53385811 6014:fcb71cb71a3d
     1 /*
       
     2  * This is the property of its developers.  See the COPYRIGHT file
       
     3  * for more details.
       
     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  *
       
    10  * This program is distributed in the hope that it will be useful,
       
    11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
       
    12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
       
    13  * GNU General Public License for more details.
       
    14  *
       
    15  * You should have received a copy of the GNU General Public License
       
    16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
       
    17  */
       
    18 
       
    19 
       
    20 #include <sys/time.h>
       
    21 
       
    22 #include "internal.h"
       
    23 #include "cipher.h"
       
    24 #include "debug.h"
       
    25 #include "util.h"
       
    26 
       
    27 #include "fbapi.h"
       
    28 
       
    29 #define PACKAGE "pidgin"
       
    30 
       
    31 #define API_URL "http://api.facebook.com/restserver.php"
       
    32 #define API_SECRET "INSERT_SECRET_HERE"
       
    33 #define MAX_CONNECTION_ATTEMPTS 3
       
    34 
       
    35 struct _PurpleFbApiCall {
       
    36 	gchar *request;
       
    37 	PurpleUtilFetchUrlData *url_data;
       
    38 	PurpleFbApiCallback callback;
       
    39 	gpointer user_data;
       
    40 	GDestroyNotify user_data_destroy_func;
       
    41 	unsigned int attempt_number;
       
    42 };
       
    43 
       
    44 static GSList *apicalls = NULL;
       
    45 
       
    46 /*
       
    47  * Try to strip characters that are not valid XML.  The string is
       
    48  * changed in-place.  This was needed because of this bug:
       
    49  * http://bugs.developers.facebook.com/show_bug.cgi?id=2840
       
    50  * That bug has been fixed, so it's possible this isn't necessary
       
    51  * anymore.
       
    52  *
       
    53  * This page lists which characters are valid:
       
    54  * http://www.w3.org/TR/2008/REC-xml-20081126/#charsets
       
    55  *
       
    56  * Valid XML characters are:
       
    57  * #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
       
    58  *
       
    59  * Invalid XML characters are:
       
    60  * [#x0-#x8] | #xB | #xC | [#xE-#x1F] | [#xD800-#xDFFF] | #xFFFE | #xFFFF
       
    61  * | [#x110000-#xFFFFFFFF]
       
    62  *
       
    63  * Note: We could maybe use purple_utf8_strip_unprintables() for this (that
       
    64  *       function was added after we had already started using this), but
       
    65  *       we know this function works and changing it is scary.
       
    66  */
       
    67 static void purple_fbapi_xml_salvage(char *str)
       
    68 {
       
    69 	gchar *tmp;
       
    70 	gunichar unichar;
       
    71 
       
    72 	for (tmp = str; tmp[0] != '\0'; tmp = g_utf8_next_char(tmp))
       
    73 	{
       
    74 		unichar = g_utf8_get_char(tmp);
       
    75 		if ((unichar >= 0x1 && unichar <= 0x8)
       
    76 				|| unichar == 0xb
       
    77 				|| unichar == 0xc
       
    78 				|| (unichar >= 0xe && unichar <= 0x1f)
       
    79 				|| (unichar >= 0xd800 && unichar <= 0xdfff)
       
    80 				|| unichar == 0xfffe
       
    81 				|| unichar == 0xffff
       
    82 				|| unichar >= 0x110000)
       
    83 		{
       
    84 			/* This character is not valid XML so replace it with question marks */
       
    85 			purple_debug_error("fbapi", "Replacing invalid "
       
    86 					"XML character %08x with question marks\n",
       
    87 					unichar);
       
    88 
       
    89 			tmp[0] = '?';
       
    90 			if (unichar & 0x0000ff00)
       
    91 				tmp[1] = '?';
       
    92 			if (unichar & 0x00ff0000)
       
    93 				tmp[2] = '?';
       
    94 			if (unichar & 0xff000000)
       
    95 				tmp[3] = '?';
       
    96 		}
       
    97 	}
       
    98 }
       
    99 
       
   100 static void purple_fbapi_request_fetch_cb(PurpleUtilFetchUrlData *url_data, gpointer user_data, const gchar *url_text, gsize len, const gchar *error_message)
       
   101 {
       
   102 	PurpleFbApiCall *apicall;
       
   103 	xmlnode *response;
       
   104 	PurpleConnectionError error = PURPLE_CONNECTION_ERROR_OTHER_ERROR;
       
   105 	char *error_message2 = NULL;
       
   106 
       
   107 	apicall = user_data;
       
   108 
       
   109 	if (error_message != NULL) {
       
   110 		/* Request failed */
       
   111 
       
   112 		if (apicall->attempt_number < MAX_CONNECTION_ATTEMPTS) {
       
   113 			/* Retry! */
       
   114 			apicall->url_data = purple_util_fetch_url_request(API_URL,
       
   115 					TRUE, NULL, FALSE, apicall->request, FALSE,
       
   116 					purple_fbapi_request_fetch_cb, apicall);
       
   117 			apicall->attempt_number++;
       
   118 			return;
       
   119 		}
       
   120 
       
   121 		response = NULL;
       
   122 		error_message2 = g_strdup(error_message);
       
   123 		error = PURPLE_CONNECTION_ERROR_NETWORK_ERROR;
       
   124 	} else if (url_text != NULL && len > 0) {
       
   125 		/* Parse the response as XML */
       
   126 		response = xmlnode_from_str(url_text, len);
       
   127 
       
   128 		if (response == NULL)
       
   129 		{
       
   130 			gchar *salvaged;
       
   131 
       
   132 			if (g_utf8_validate(url_text, len, NULL)) {
       
   133 				salvaged = g_strdup(url_text);
       
   134 			} else {
       
   135 				/* Facebook responded with invalid UTF-8.  Bastards. */
       
   136 				purple_debug_error("fbapi", "Response is not valid UTF-8\n");
       
   137 				salvaged = purple_utf8_salvage(url_text);
       
   138 			}
       
   139 
       
   140 			purple_fbapi_xml_salvage(salvaged);
       
   141 			response = xmlnode_from_str(salvaged, -1);
       
   142 			g_free(salvaged);
       
   143 		}
       
   144 
       
   145 		if (response == NULL) {
       
   146 			purple_debug_error("fbapi", "Could not parse response as XML: %*s\n",
       
   147 			(int)len, url_text);
       
   148 			error_message2 = g_strdup(_("Invalid response from server"));
       
   149 		} else if (g_str_equal(response->name, "error_response")) {
       
   150 			/*
       
   151 			 * The response is an error message, in the standard format
       
   152 			 * for errors from API calls.
       
   153 			 */
       
   154 			xmlnode *tmp;
       
   155 			char *tmpstr;
       
   156 
       
   157 			tmp = xmlnode_get_child(response, "error_code");
       
   158 			if (tmp != NULL) {
       
   159 				tmpstr = xmlnode_get_data_unescaped(tmp);
       
   160 				if (tmpstr != NULL && strcmp(tmpstr, "293") == 0) {
       
   161 					error_message2 = g_strdup(_("Need chat permission"));
       
   162 					error = PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED;
       
   163 				}
       
   164 				g_free(tmpstr);
       
   165 			}
       
   166 			if (error_message2 == NULL) {
       
   167 				error = PURPLE_CONNECTION_ERROR_OTHER_ERROR;
       
   168 				tmp = xmlnode_get_child(response, "error_msg");
       
   169 				if (tmp != NULL)
       
   170 					error_message2 = xmlnode_get_data_unescaped(tmp);
       
   171 			}
       
   172 			if (error_message2 == NULL)
       
   173 				error_message2 = g_strdup(_("Unknown"));
       
   174 		} else {
       
   175 			error_message2 = NULL;
       
   176 		}
       
   177 	} else {
       
   178 		/* Response body was empty */
       
   179 		response = NULL;
       
   180 		error_message2 = NULL;
       
   181 	}
       
   182 
       
   183 	if (apicall->attempt_number > 1 || error_message2 != NULL)
       
   184 		purple_debug_error("fbapi", "Request '%s' %s after %u attempts: %s\n",
       
   185 				apicall->request,
       
   186 				error_message == NULL ? "succeeded" : "failed",
       
   187 				apicall->attempt_number, error_message2);
       
   188 
       
   189 	/*
       
   190 	 * The request either succeeded or failed the maximum number of
       
   191 	 * times.  In either case, pass control off to the callback
       
   192 	 * function and let them decide what to do.
       
   193 	 */
       
   194 	apicall->callback(apicall, apicall->user_data, response, error, error_message2);
       
   195 	apicall->url_data = NULL;
       
   196 	purple_fbapi_request_destroy(apicall);
       
   197 
       
   198 	xmlnode_free(response);
       
   199 	g_free(error_message2);
       
   200 }
       
   201 
       
   202 static gboolean concat_params(gpointer key, gpointer value, gpointer data)
       
   203 {
       
   204 	GString *tmp;
       
   205 
       
   206 	tmp = data;
       
   207 	g_string_append_printf(tmp, "%s=%s", (const char *)key, (const char *)value);
       
   208 
       
   209 	return FALSE;
       
   210 }
       
   211 
       
   212 /**
       
   213  * @return A Newly allocated base16 encoded version of the md5
       
   214  *         signature calculated using the algorithm described on the
       
   215  *         Facebook developer wiki.  This string must be g_free'd.
       
   216  */
       
   217 static char *generate_signature(const char *api_secret, const GTree *params)
       
   218 {
       
   219 	GString *tmp;
       
   220 	unsigned char hashval[16];
       
   221 
       
   222 	tmp = g_string_new(NULL);
       
   223 	g_tree_foreach((GTree *)params, concat_params, tmp);
       
   224 	g_string_append(tmp, api_secret);
       
   225 
       
   226 	purple_cipher_digest_region("md5", (const unsigned char *)tmp->str,
       
   227 			tmp->len, sizeof(hashval), hashval, NULL);
       
   228 	g_string_free(tmp, TRUE);
       
   229 
       
   230 	return purple_base16_encode(hashval, sizeof(hashval));
       
   231 }
       
   232 
       
   233 static gboolean append_params_to_body(gpointer key, gpointer value, gpointer data)
       
   234 {
       
   235 	GString *body;
       
   236 
       
   237 	body = data;
       
   238 
       
   239 	if (body->len > 0)
       
   240 		g_string_append_c(body, '&');
       
   241 
       
   242 	g_string_append(body, purple_url_encode(key));
       
   243 	g_string_append_c(body, '=');
       
   244 	g_string_append(body, purple_url_encode(value));
       
   245 
       
   246 	return FALSE;
       
   247 }
       
   248 
       
   249 static GString *purple_fbapi_construct_request_vargs(PurpleAccount *account, const char *method, va_list args)
       
   250 {
       
   251 	GTree *params;
       
   252 	const char *api_key, *api_secret;
       
   253 	const char *key, *value;
       
   254 	char call_id[21];
       
   255 	char *signature;
       
   256 	GString *body;
       
   257 
       
   258 	/* Read all paramters into a sorted tree */
       
   259 	params = g_tree_new((GCompareFunc)strcmp);
       
   260 	while ((key = va_arg(args, const char *)) != NULL)
       
   261 	{
       
   262 		value = va_arg(args, const char *);
       
   263 		g_tree_insert(params, (char *)key, (char *)value);
       
   264 
       
   265 		/* If we have an access_token then we need a call_id */
       
   266 		if (g_str_equal(key, "access_token")) {
       
   267 			struct timeval tv;
       
   268 			if (gettimeofday(&tv, NULL) != 0) {
       
   269 				time_t now;
       
   270 				purple_debug_error("fbapi",
       
   271 						"Error calling gettimeofday(): %s\n",
       
   272 						g_strerror(errno));
       
   273 				now = time(NULL);
       
   274 				strftime(call_id, sizeof(call_id), "%s000000", localtime(&now));
       
   275 			} else {
       
   276 				char tmp[22];
       
   277 				strftime(tmp, sizeof(tmp), "%s", localtime(&tv.tv_sec));
       
   278 				sprintf(call_id, "%s%06lu", tmp, (long unsigned int)tv.tv_usec);
       
   279 			}
       
   280 			g_tree_insert(params, "call_id", call_id);
       
   281 		}
       
   282 	}
       
   283 
       
   284 	api_key = purple_account_get_string(account, "fb_api_key", PURPLE_FBAPI_KEY);
       
   285 	api_secret = purple_account_get_string(account, "fb_api_secret", API_SECRET);
       
   286 
       
   287 	/* Add the method and api_key parameters to the list */
       
   288 	g_tree_insert(params, "method", (char *)method);
       
   289 	g_tree_insert(params, "api_key", (char *)api_key);
       
   290 
       
   291 	/* Add the signature parameter to the list */
       
   292 	signature = generate_signature((char *)api_secret, params);
       
   293 	g_tree_insert(params, "sig", signature);
       
   294 
       
   295 	/* Construct the body of the HTTP POST request */
       
   296 	body = g_string_new(NULL);
       
   297 	g_tree_foreach(params, append_params_to_body, body);
       
   298 	g_tree_destroy(params);
       
   299 	g_free(signature);
       
   300 
       
   301 	return body;
       
   302 }
       
   303 
       
   304 GString *purple_fbapi_construct_request(PurpleAccount *account, const char *method, ...)
       
   305 {
       
   306 	va_list args;
       
   307 	GString *body;
       
   308 
       
   309 	va_start(args, method);
       
   310 	body = purple_fbapi_construct_request_vargs(account, method, args);
       
   311 	va_end(args);
       
   312 
       
   313 	return body;
       
   314 }
       
   315 
       
   316 PurpleFbApiCall *purple_fbapi_request_vargs(PurpleAccount *account, PurpleFbApiCallback callback, gpointer user_data, GDestroyNotify user_data_destroy_func, const char *method, va_list args)
       
   317 {
       
   318 	GString *body;
       
   319 	PurpleFbApiCall *apicall;
       
   320 
       
   321 	body = purple_fbapi_construct_request_vargs(account, method, args);
       
   322 
       
   323 	/* Construct an HTTP POST request */
       
   324 	apicall = g_new(PurpleFbApiCall, 1);
       
   325 	apicall->callback = callback;
       
   326 	apicall->user_data = user_data;
       
   327 	apicall->user_data_destroy_func = user_data_destroy_func;
       
   328 	apicall->attempt_number = 1;
       
   329 
       
   330 	apicall->request = g_strdup_printf("POST /restserver.php HTTP/1.0\r\n"
       
   331 			"Connection: close\r\n"
       
   332 			"Accept: */*\r\n"
       
   333 			"Content-Type: application/x-www-form-urlencoded; charset=UTF-8\r\n"
       
   334 			"Content-Length: %zu\r\n\r\n%s", (size_t)body->len, body->str);
       
   335 	g_string_free(body, TRUE);
       
   336 
       
   337 	apicall->url_data = purple_util_fetch_url_request(API_URL,
       
   338 			TRUE, NULL, FALSE, apicall->request, FALSE,
       
   339 			purple_fbapi_request_fetch_cb, apicall);
       
   340 
       
   341 	apicalls = g_slist_prepend(apicalls, apicall);
       
   342 
       
   343 	return apicall;
       
   344 }
       
   345 
       
   346 PurpleFbApiCall *purple_fbapi_request(PurpleAccount *account, PurpleFbApiCallback callback, gpointer user_data, GDestroyNotify user_data_destroy_func, const char *method, ...)
       
   347 {
       
   348 	va_list args;
       
   349 	PurpleFbApiCall *apicall;
       
   350 
       
   351 	va_start(args, method);
       
   352 	apicall = purple_fbapi_request_vargs(account, callback, user_data, user_data_destroy_func, method, args);
       
   353 	va_end(args);
       
   354 
       
   355 	return apicall;
       
   356 }
       
   357 
       
   358 void purple_fbapi_request_destroy(PurpleFbApiCall *apicall)
       
   359 {
       
   360 	apicalls = g_slist_remove(apicalls, apicall);
       
   361 
       
   362 	if (apicall->url_data != NULL)
       
   363 		purple_util_fetch_url_cancel(apicall->url_data);
       
   364 
       
   365 	if (apicall->user_data != NULL && apicall->user_data_destroy_func != NULL)
       
   366 		apicall->user_data_destroy_func(apicall->user_data);
       
   367 
       
   368 	g_free(apicall->request);
       
   369 	g_free(apicall);
       
   370 }
       
   371 
       
   372 void purple_fbapi_uninit(void)
       
   373 {
       
   374 	while (apicalls != NULL)
       
   375 		purple_fbapi_request_destroy(apicalls->data);
       
   376 }