1 /// Implementations of OAuth 1.0 server and client. You probably don't need this anymore; I haven't used it for years.
2 module arsd.oauth;
3 
4 import arsd.curl;
5 import arsd.uri;
6 import arsd.cgi : Cgi;
7 import std.array;
8 static import std.uri;
9 static import std.algorithm;
10 import std.conv;
11 import std.string;
12 import std.random;
13 import std.base64;
14 import std.exception;
15 import std.datetime;
16 
17 
18 static if(__VERSION__ <= 2076) {
19 	// compatibility shims with gdc
20 	enum JSONType {
21 		object = JSON_TYPE.OBJECT,
22 		null_ = JSON_TYPE.NULL,
23 		false_ = JSON_TYPE.FALSE,
24 		true_ = JSON_TYPE.TRUE,
25 		integer = JSON_TYPE.INTEGER,
26 		float_ = JSON_TYPE.FLOAT,
27 		array = JSON_TYPE.ARRAY,
28 		string = JSON_TYPE.STRING,
29 		uinteger = JSON_TYPE.UINTEGER
30 	}
31 }
32 
33 
34 ///////////////////////////////////////
35 
36 class FacebookApiException : Exception {
37 	public this(string response, string token = null, string scopeRequired = null) {
38 		this.token = token;
39 		this.scopeRequired = scopeRequired;
40 		super(response ~ "\nToken: " ~ token ~ "\nScope: " ~ scopeRequired);
41 	}
42 
43 	string token;
44 	string scopeRequired;
45 }
46 
47 
48 import arsd.curl;
49 import arsd.sha;
50 
51 import std.digest.md;
52 
53 import std.file;
54 
55 
56 // note when is a d_time, so unix_timestamp * 1000
57 Variant[string] postToFacebookWall(string[] info, string id, string message, string picture = null, string link = null, long when = 0, string linkDescription = null) {
58 	string url = "https://graph.facebook.com/" ~ id ~ "/feed";
59 
60 	string data = "access_token=" ~ std.uri.encodeComponent(info[1]);
61 	data ~= "&message=" ~ std.uri.encodeComponent(message);
62 
63 	if(picture !is null && picture.length)
64 		data ~= "&picture=" ~ std.uri.encodeComponent(picture);
65 	if(link !is null && link.length)
66 		data ~= "&link=" ~ std.uri.encodeComponent(link);
67 	if(when) {
68 		data ~= "&scheduled_publish_time=" ~ to!string(when / 1000);
69 		data ~= "&published=false";
70 	}
71 	if(linkDescription.length)
72 		data ~= "&description=" ~ std.uri.encodeComponent(linkDescription);
73 
74 	auto response = curl(url, data);
75 
76 	auto res = jsonToVariant(response);
77 /+
78 {"error":{"type":"OAuthException","message":"An active access token must be used to query information about the current user."}}
79 +/
80 	// assert(0, response);
81 
82 	auto var = res.get!(Variant[string]);
83 
84 
85 	if("error" in var) {
86 		auto error = var["error"].get!(Variant[string]);
87 
88 		throw new FacebookApiException(error["message"].get!string, info[1],
89 			"scope" in error ? error["scope"].get!string : "publish_stream");
90 	}
91 
92 	return var;
93 }
94 
95 version(with_arsd_jsvar) {
96 	import arsd.jsvar;
97 	var fbGraph(string token, string id, bool useCache = false, long maxCacheHours = 2) {
98 		auto response = fbGraphImpl(token, id, useCache, maxCacheHours);
99 
100 		var ret = var.emptyObject;
101 
102 		if(response == "false") {
103 			var v1 = id[1..$];
104 			ret["id"] = v1;
105 			ret["name"] = v1 = "Private";
106 			ret["description"] = v1 = "This is a private facebook page. Please make it public in Facebook if you want to promote it.";
107 			ret["link"] = v1 = "http://facebook.com?profile.php?id=" ~ id[1..$];
108 			ret["is_false"] = true;
109 			return ret;
110 		}
111 
112 		ret = var.fromJson(response);
113 
114 		if("error" in ret) {
115 			auto error = ret.error;
116 
117 			if("message" in error)
118 				throw new FacebookApiException(error["message"].get!string, token.length > 1 ? token : null,
119 					"scope" in error ? error["scope"].get!string : null);
120 			else
121 				throw new FacebookApiException("couldn't get FB info");
122 		}
123 
124 		return ret;
125 	}
126 }
127 
128 Variant[string] fbGraph(string[] info, string id, bool useCache = false, long maxCacheHours = 2) {
129 	auto response = fbGraphImpl(info[1], id, useCache, maxCacheHours);
130 
131 	if(response == "false") {
132 		//throw new Exception("This page is private. Please make it public in Facebook.");
133 		// we'll make dummy data so this still returns
134 
135 		Variant[string] ret;
136 
137 		Variant v1 = id[1..$];
138 		ret["id"] = v1;
139 		ret["name"] = v1 = "Private";
140 		ret["description"] = v1 = "This is a private facebook page. Please make it public in Facebook if you want to promote it.";
141 		ret["link"] = v1 = "http://facebook.com?profile.php?id=" ~ id[1..$];
142 		ret["is_false"] = true;
143 		return ret;
144 	}
145 
146 	auto res = jsonToVariant(response);
147 /+
148 {"error":{"type":"OAuthException","message":"An active access token must be used to query information about the current user."}}
149 +/
150 	// assert(0, response);
151 
152 	auto var = res.get!(Variant[string]);
153 
154 	if("error" in var) {
155 		auto error = var["error"].get!(Variant[string]);
156 
157 		if("message" in error)
158 		throw new FacebookApiException(error["message"].get!string, info.length > 1 ? info[1] : null,
159 			"scope" in error ? error["scope"].get!string : null);
160 		else
161 			throw new FacebookApiException("couldn't get FB info");
162 	}
163 
164 	return var;
165 
166 }
167 
168 // note ids=a,b,c works too. it returns an associative array of the ids requested.
169 string fbGraphImpl(string info, string id, bool useCache = false, long maxCacheHours = 2) {
170 	string response;
171 
172 	string cacheFile;
173 
174 	char c = '?';
175 
176 	if(id.indexOf("?") != -1)
177 		c = '&';
178 
179 	string url;
180 
181 	if(id[0] != '/')
182 		id = "/" ~ id;
183 
184 	if(info !is null)
185 		url = "https://graph.facebook.com" ~ id
186 			~ c ~ "access_token=" ~ info ~ "&format=json";
187 	else
188 		url = "http://graph.facebook.com" ~ id
189 			~ c ~ "format=json";
190 
191 	// this makes pagination easier. the initial / is there because it is added above
192 	if(id.indexOf("/http://") == 0 || id.indexOf("/https://") == 0)
193 		url = id[1 ..$];
194 
195 	if(useCache)
196 		cacheFile = "/tmp/fbGraphCache-" ~ hashToString(SHA1(url));
197 
198 	if(useCache) {
199 		if(std.file.exists(cacheFile)) {
200 			if((Clock.currTime() - std.file.timeLastModified(cacheFile)) < dur!"hours"(maxCacheHours)) {
201 				response = std.file.readText(cacheFile);
202 				goto haveResponse;
203 			}
204 		}
205 	}
206 
207 	try {
208 		response = curl(url);
209 	} catch(CurlException e) {
210 		throw new FacebookApiException(e.msg);
211 	}
212 
213 	if(useCache) {
214 		std.file.write(cacheFile, response);
215 	}
216 
217     haveResponse:
218 	assert(response.length);
219 
220 	return response;
221 }
222 
223 
224 
225 string[string][] getBasicDataFromVariant(Variant[string] v) {
226 	auto items = v["data"].get!(Variant[]);
227 	return getBasicDataFromVariant(items);
228 }
229 
230 string[string][] getBasicDataFromVariant(Variant[] items) {
231 	string[string][] ret;
232 
233 	foreach(item; items) {
234 		auto i = item.get!(Variant[string]);
235 
236 		string[string] l;
237 
238 		foreach(k, v; i) {
239 			l[k] = to!string(v);
240 		}
241 
242 		ret ~= l;
243 	}
244 
245 	return ret;
246 }
247 
248 
249 /////////////////////////////////////
250 
251 
252 
253 
254 
255 
256 
257 
258 
259 
260 
261 /* ******************************* */
262 
263 /*         OAUTH   1.0             */
264 
265 /* ******************************* */
266 
267 
268 struct OAuthParams {
269 	string apiKey;
270 	string apiSecret;
271 	string baseUrl;
272 	string requestTokenPath;
273 	string accessTokenPath;
274 	string authorizePath;
275 }
276 
277 OAuthParams twitter(string apiKey, string apiSecret) {
278 	OAuthParams params;
279 
280 	params.apiKey = apiKey;
281 	params.apiSecret = apiSecret;
282 
283 	params.baseUrl = "https://api.twitter.com";
284 	//params.baseUrl = "http://twitter.com";
285 	params.requestTokenPath = "/oauth/request_token";
286 	params.authorizePath = "/oauth/authorize";
287 	params.accessTokenPath = "/oauth/access_token";
288 
289 	return params;
290 }
291 
292 OAuthParams tumblr(string apiKey, string apiSecret) {
293 	OAuthParams params;
294 
295 	params.apiKey = apiKey;
296 	params.apiSecret = apiSecret;
297 
298 	params.baseUrl = "http://www.tumblr.com";
299 	params.requestTokenPath = "/oauth/request_token";
300 	params.authorizePath = "/oauth/authorize";
301 	params.accessTokenPath = "/oauth/access_token";
302 
303 	return params;
304 }
305 
306 OAuthParams linkedIn(string apiKey, string apiSecret) {
307 	OAuthParams params;
308 
309 	params.apiKey = apiKey;
310 	params.apiSecret = apiSecret;
311 
312 	params.baseUrl = "https://api.linkedin.com";
313 	params.requestTokenPath = "/uas/oauth/requestToken";
314 	params.accessTokenPath = "/uas/oauth/accessToken";
315 	params.authorizePath = "/uas/oauth/authorize";
316 
317 	return params;
318 }
319 
320 OAuthParams aWeber(string apiKey, string apiSecret) {
321 	OAuthParams params;
322 
323 	params.apiKey = apiKey;
324 	params.apiSecret = apiSecret;
325 
326 	params.baseUrl = "https://auth.aweber.com";
327 	params.requestTokenPath = "/1.1/oauth/request_token";
328 	params.accessTokenPath = "/1.1/oauth/access_token";
329 	params.authorizePath = "/1.1/oauth/authorize";
330 
331 	// API Base: https://api.aweber.com/1.0/
332 
333 	return params;
334 }
335 
336 
337 string tweet(OAuthParams params, string oauthToken, string tokenSecret, string message) {
338 	assert(oauthToken.length);
339 	assert(tokenSecret.length);
340 
341 	auto args = [
342 		"oauth_token" : oauthToken,
343 		"token_secret" : tokenSecret,
344 	];
345 
346 	auto data = "status=" ~ rawurlencode(message);//.replace("%3F", "?");//encodeVariables(["status" : message]);
347 
348 	auto ret = curlOAuth(params, "https://api.twitter.com" ~ "/1.1/statuses/update.json", args, "POST", data);
349 
350 	auto val = jsonToVariant(ret).get!(Variant[string]);
351 	if("id_str" !in val)
352 		throw new Exception("bad result from twitter: " ~ ret);
353 	return val["id_str"].get!string;
354 }
355 
356 import std.file;
357 /**
358 	Redirects the user to the authorize page on the provider's website.
359 */
360 void authorizeStepOne(Cgi cgi, OAuthParams params, string oauthCallback = null, string additionalOptions = null, string[string] additionalTokenArgs = null) {
361 	if(oauthCallback is null) {
362 		oauthCallback = cgi.getCurrentCompleteUri();
363 		if(oauthCallback.indexOf("?") == -1)
364 			oauthCallback ~= "?oauth_step=two";
365 		else
366 			oauthCallback ~= "&oauth_step=two";
367 	}
368 
369 	string[string] args;
370 	if(oauthCallback.length)
371 		args["oauth_callback"] = oauthCallback;
372 
373 	//foreach(k, v; additionalTokenArgs)
374 		//args[k] = v;
375 
376 	auto moreArgs = encodeVariables(additionalTokenArgs);
377 	if(moreArgs.length)
378 		moreArgs = "?" ~ moreArgs;
379 	auto ret = curlOAuth(params, params.baseUrl ~ params.requestTokenPath ~ moreArgs,
380 	 		args, "POST", "", "");
381 	auto vals = decodeVariables(ret);
382 
383 	if("oauth_problem" in vals)
384 		throw new Exception("OAuth problem: " ~ vals["oauth_problem"][0]);
385 
386 	if(vals.keys.length < 2)
387 		throw new Exception(ret);
388 
389 	///vals["fuck_you"] = [params.baseUrl ~ params.requestTokenPath];
390 
391 	auto oauth_token = vals["oauth_token"][0];
392 	auto oauth_secret = vals["oauth_token_secret"][0];
393 
394 	// need to save the secret for later
395 	std.file.write("/tmp/oauth-token-secret-" ~ oauth_token,
396 		oauth_secret);
397 
398 	// FIXME: make sure this doesn't break twitter etc
399 	if("login_url" in vals) // apparently etsy does it this way...
400 		cgi.setResponseLocation(vals["login_url"][0]);
401 	else
402 		cgi.setResponseLocation(params.baseUrl ~ params.authorizePath ~ "?" ~(additionalOptions.length ? (additionalOptions ~ "&") : "")~ "oauth_token=" ~ oauth_token);
403 }
404 
405 /**
406 	Gets the final token, given the stuff from step one. This should be called
407 	from the callback in step one.
408 
409 	Returns [token, secret, raw original data (for extended processing - twitter also sends the screen_name and user_id there)]
410 */
411 string[] authorizeStepTwo(const(Cgi) cgi, OAuthParams params) {
412 	if("oauth_problem" in cgi.get)
413 		throw new Exception("OAuth problem: " ~ cgi.get["oauth_problem"]);
414 
415 	string token = cgi.get["oauth_token"];
416 	string verifier = cgi.get["oauth_verifier"];
417 
418 	// reload from file written above. FIXME: clean up old shit too
419 	string secret = std.file.readText("/tmp/oauth-token-secret-" ~ token);
420 	// don't need it anymore...
421 	std.file.remove("/tmp/oauth-token-secret-" ~ token);
422 
423 
424 	auto ret = curlOAuth(params, params.baseUrl ~ params.accessTokenPath,
425 		["oauth_token" : token,
426 		 "oauth_verifier" : verifier,
427 		 "token_secret" : secret], "POST", "", "");
428 
429 	auto vars = decodeVariables(ret);
430 
431 	return [vars["oauth_token"][0], vars["oauth_token_secret"][0], ret];
432 }
433 
434 
435 
436 /**
437 	Note in oauthValues:
438 		It creates the nonce, signature_method, version, consumer_key, and timestamp
439 		ones inside this function - you don't have to do it.
440 
441 		Just put in the values specific to your call.
442 
443 	oauthValues["token_secret"] if present, is concated into the signing string. Don't
444 	put it in for the early steps!
445 */
446 
447 import core.stdc.stdlib;
448 
449 string curlOAuth(OAuthParams auth, string url, string[string] oauthValues, string method = null,string data = null, string contentType = "application/x-www-form-urlencoded") {
450 
451 	//string oauth_callback; // from user
452 
453 	oauthValues["oauth_consumer_key"] = 	auth.apiKey;
454 	oauthValues["oauth_nonce"] = 		makeNonce();
455 	oauthValues["oauth_signature_method"] = "HMAC-SHA1";
456 
457 	oauthValues["oauth_timestamp"] = 	to!string(Clock.currTime().toUTC().toUnixTime());
458 	oauthValues["oauth_version"] = 		"1.0";
459 
460 	auto questionMark = std..string.indexOf(url, "?");
461 
462 	string signWith = std.uri.encodeComponent(auth.apiSecret) ~ "&";
463 	if("token_secret" in oauthValues) {
464 		signWith ~= std.uri.encodeComponent(oauthValues["token_secret"]);
465 		oauthValues.remove("token_secret");
466 	}
467 
468 	if(method is null)
469 		method = data is null ? "GET" : "POST";
470 
471 	auto baseString = getSignatureBaseString(
472 			method,
473 			questionMark == -1 ? url : url[0..questionMark],
474 			questionMark == -1 ? "" : url[questionMark+1 .. $],
475 			oauthValues,
476 			contentType == "application/x-www-form-urlencoded" ? data : null
477 		);
478 
479 	string oauth_signature = /*std.uri.encodeComponent*/(cast(string)
480 	    Base64.encode(mhashSign(baseString, signWith, MHASH_SHA1)));
481 
482 	oauthValues["oauth_signature"] = oauth_signature;
483 
484 	string oauthHeader;
485 	bool outputted = false;
486 	Pair[] pairs;
487 	foreach(k, v; oauthValues) {
488 		pairs ~= Pair(k, v);
489 	}
490 
491 	foreach(pair; std.algorithm.sort(pairs)) {
492 		if(outputted)
493 			oauthHeader ~= ", ";
494 		else
495 			outputted = true;
496 
497 		oauthHeader ~= pair.output(true);
498 	}
499 
500 	return curlAuth(url, data, null, null, contentType, method, ["Authorization: OAuth " ~ oauthHeader]);
501 }
502 
503 bool isOAuthRequest(Cgi cgi) {
504 	if(cgi.authorization.length < 5 || cgi.authorization[0..5] != "OAuth")
505 		return false;
506 	return true;
507 }
508 
509 string getApiKeyFromRequest(Cgi cgi) {
510 	enforce(isOAuthRequest(cgi));
511 	auto variables = split(cgi.authorization[6..$], ",");
512 
513 	foreach(var; variables)
514 		if(var.startsWith("oauth_consumer_key"))
515 			return var["oauth_consumer_key".length + 3 .. $ - 1]; // trimming quotes too
516 	throw new Exception("api key not present");
517 }
518 
519 string getTokenFromRequest(Cgi cgi) {
520 	enforce(isOAuthRequest(cgi));
521 	auto variables = split(cgi.authorization[6..$], ",");
522 
523 	foreach(var; variables)
524 		if(var.startsWith("oauth_token"))
525 			return var["oauth_token".length + 3 .. $ - 1]; // trimming quotes too
526 	return null;
527 }
528 
529 // FIXME check timestamp and maybe nonce too
530 
531 bool isSignatureValid(Cgi cgi, string apiSecret, string tokenSecret) {
532 	enforce(isOAuthRequest(cgi));
533 	auto variables = split(cgi.authorization[6..$], ",");
534 
535 	string[string] oauthValues;
536 	foreach(var; variables) {
537 		auto it = var.split("=");
538 		oauthValues[it[0]] = it[1][1 .. $ - 1]; // trimming quotes
539 	}
540 
541 	auto url = cgi.getCurrentCompleteUri();
542 
543 	auto questionMark = std..string.indexOf(url, "?");
544 
545 	string signWith = std.uri.encodeComponent(apiSecret) ~ "&";
546 	if(tokenSecret.length)
547 		signWith ~= std.uri.encodeComponent(tokenSecret);
548 
549 	auto method = to!string(cgi.requestMethod);
550 
551 	if("oauth_signature" !in oauthValues)
552 		return false;
553 
554 	auto providedSignature = oauthValues["oauth_signature"];
555 
556 	oauthValues.remove("oauth_signature");
557 
558 	string oauth_signature = std.uri.encodeComponent(cast(string)
559 	    Base64.encode(mhashSign(
560 		getSignatureBaseString(
561 			method,
562 			questionMark == -1 ? url : url[0..questionMark],
563 			questionMark == -1 ? "" : url[questionMark+1 .. $],
564 			oauthValues,
565 			cgi.postArray // FIXME: if this was a file upload, this isn't actually right
566 		), signWith, MHASH_SHA1)));
567 
568 	return oauth_signature == providedSignature;
569 
570 }
571 
572 string makeNonce() {
573 	auto val = to!string(uniform(uint.min, uint.max)) ~ to!string(Clock.currTime().stdTime);
574 
575 	return val;
576 }
577 
578 struct Pair {
579 	string name;
580 	string value;
581 
582 	string output(bool useQuotes = false) {
583 		if(useQuotes)
584 			return std.uri.encodeComponent(name) ~ "=\"" ~ rawurlencode(value) ~ "\"";
585 		else
586 			return std.uri.encodeComponent(name) ~ "=" ~ rawurlencode(value);
587 	}
588 
589 	int opCmp(Pair rhs) {
590 		// FIXME: is name supposed to be encoded?
591 		int val = std..string.cmp(name, rhs.name);
592 
593 		if(val == 0)
594 			val = std..string.cmp(value, rhs.value);
595 
596 		return val;
597 	}
598 }
599 string getSignatureBaseString(
600 	string method,
601 	string protocolHostAndPath,
602 	string queryStringContents,
603 	string[string] authorizationHeaderContents,
604 	in string[][string] postArray)
605 {
606 	string baseString;
607 
608 	baseString ~= method;
609 	baseString ~= "&";
610 	baseString ~= std.uri.encodeComponent(protocolHostAndPath);
611 	baseString ~= "&";
612 
613 	auto getArray = decodeVariables(queryStringContents);
614 
615 	Pair[] pairs;
616 
617 	foreach(k, vals; getArray)
618 		foreach(v; vals)
619 			pairs ~= Pair(k, v);
620 	foreach(k, vals; postArray)
621 		foreach(v; vals)
622 			pairs ~= Pair(k, v);
623 	foreach(k, v; authorizationHeaderContents)
624 		pairs ~= Pair(k, v);
625 
626 	bool outputted = false;
627 
628 	string params;
629 	foreach(pair; std.algorithm.sort(pairs)) {
630 		if(outputted)
631 			params ~= "&";
632 		else
633 			outputted = true;
634 		params ~= pair.output();
635 	}
636 
637 	baseString ~= std.uri.encodeComponent(params);
638 
639 	return baseString;
640 }
641 
642 
643 string getSignatureBaseString(
644 	string method,
645 	string protocolHostAndPath,
646 	string queryStringContents,
647 	string[string] authorizationHeaderContents,
648 	string postBodyIfWwwEncoded)
649 {
650 	return getSignatureBaseString(
651 		method,
652 		protocolHostAndPath,
653 		queryStringContents,
654 		authorizationHeaderContents,
655 		decodeVariables(postBodyIfWwwEncoded));
656 }
657 
658 /***************************************/
659 
660 //     OAuth 2.0 as used by Facebook   //
661 
662 /***************************************/
663 
664 immutable(ubyte)[] base64UrlDecode(string e) {
665 	string encoded = e.idup;
666 	while (encoded.length % 4) {
667 		encoded ~= "="; // add padding
668 	}
669 
670 	// convert base64 URL to standard base 64
671 	encoded = encoded.replace("-", "+");
672 	encoded = encoded.replace("_", "/");
673 
674 	auto ugh = Base64.decode(encoded);
675 	return assumeUnique(ugh);
676 }
677 
678 Ret parseSignedRequest(Ret = Variant)(in string req, string apisecret) {
679 	auto parts = req.split(".");
680 
681 	immutable signature = parts[0];
682 	immutable jsonEncoded = parts[1];
683 
684 	auto expected = mhashSign(jsonEncoded, apisecret, MHASH_SHA256);
685 	auto got = base64UrlDecode(signature);
686 
687 	enforce(expected == got, "Signatures didn't match");
688 
689 	auto json = cast(string) base64UrlDecode(jsonEncoded);
690 
691 	static if(is(Ret == Variant))
692 		return jsonToVariant(json);
693 	else
694 		return Ret.fromJson(json);
695 }
696 
697 string stripWhitespace(string w) {
698 	return w.replace("\t", "").replace("\n", "").replace(" ", "");
699 }
700 
701 string translateCodeToAccessToken(string code, string redirectUrl, string appId, string apiSecret) {
702 	string res = curl(stripWhitespace("https://graph.facebook.com/oauth/access_token?
703 		client_id="~appId~"&redirect_uri="~std.uri.encodeComponent(redirectUrl)~"&
704 		client_secret="~apiSecret~"&code=" ~ std.uri.encodeComponent(code)
705 	));
706 
707 	if(res.indexOf("access_token=") == -1) {
708 		throw new Exception("Couldn't translate code to access token. [" ~ res ~ "]");
709 	}
710 
711 	auto vars = decodeVariablesSingle(res);
712 	return vars["access_token"];
713 }
714 
715 /+
716 
717 void updateFbGraphPermissions(string token) {
718 	fbGraph([null, token], "/me/permissions", true, -1); // use the cache, but only read if it is in the future - basically, force a cache refresh
719 	fbGraph([null, token], "/me/friends", true, -1); // do the same thing for friends..
720 }
721 
722 auto fbGraphPermissions(string token) {
723 	return fbGraph([null, token], "/me/permissions", true, 36); // use the cache
724 }
725 
726 enum FacebookPermissions {
727 	user_likes,
728 	friends_likes,
729 	publish_stream,
730 	publish_actions,
731 	offline_access,
732 	manage_pages,
733 }
734 
735 bool hasPermission(DataObject person, FacebookPermissions permission) {
736 	version(live) {} else return true; // on dev, just skip this stuff
737 
738 	if(person.facebook_access_token.length == 0)
739 		return false;
740 	try {
741 		auto perms = getBasicDataFromVariant(fbGraphPermissions(person.                       facebook_access_token))[0];
742 		return (to!string(permission) in perms) ? true : false;
743 	} catch(FacebookApiException e) {
744 		return false; // the token doesn't work
745 	}
746 
747 	return false;
748 }
749 
750 +/
751 
752 
753 /****************************************/
754 
755 //      Generic helper functions for web work
756 
757 /****************************************/
758 
759 import std.variant;
760 import std.json;
761 
762 Variant jsonToVariant(string json) {
763 	auto decoded = parseJSON(json);
764 	return jsonValueToVariant(decoded);
765 }
766 
767 Variant jsonValueToVariant(JSONValue v) {
768 	Variant ret;
769 
770 	final switch(v.type) {
771 		case JSONType..string:
772 			ret = v.str;
773 		break;
774 		case JSONType.uinteger:
775 			ret = v.uinteger;
776 		break;
777 		case JSONType.integer:
778 			ret = v.integer;
779 		break;
780 		case JSONType.float_:
781 			ret = v.floating;
782 		break;
783 		case JSONType.object:
784 			Variant[string] obj;
785 			foreach(k, val; v.object) {
786 				obj[k] = jsonValueToVariant(val);
787 			}
788 
789 			ret = obj;
790 		break;
791 		case JSONType.array:
792 			Variant[] arr;
793 			foreach(i; v.array) {
794 				arr ~= jsonValueToVariant(i);
795 			}
796 
797 			ret = arr;
798 		break;
799 		case JSONType.true_:
800 			ret = true;
801 		break;
802 		case JSONType.false_:
803 			ret = false;
804 		break;
805 		case JSONType.null_:
806 			ret = null;
807 		break;
808 	}
809 
810 	return ret;
811 }
812 
813 /***************************************/
814 
815 //       Interface to C lib for signing
816 
817 /***************************************/
818 
819 extern(C) {
820 	alias int hashid;
821 	MHASH mhash_hmac_init(hashid, const scope void*, int, int);
822 	bool mhash(const scope void*, const scope void*, int);
823 	int mhash_get_hash_pblock(hashid);
824 	byte* mhash_hmac_end(MHASH);
825 	int mhash_get_block_size(hashid);
826 
827 	hashid MHASH_MD5 = 1;
828 	hashid MHASH_SHA1 = 2;
829 	hashid MHASH_SHA256 = 17;
830 	alias void* MHASH;
831 }
832 
833 ubyte[] mhashSign(string data, string signWith, hashid algorithm) @trusted {
834         auto td = mhash_hmac_init(algorithm, signWith.ptr, cast(int) signWith.length,
835                             mhash_get_hash_pblock(algorithm));
836 
837         mhash(td, data.ptr, cast(int) data.length);
838 	auto mac = mhash_hmac_end(td);
839 	ubyte[] ret;
840 
841         for (int j = 0; j < mhash_get_block_size(algorithm); j++) {
842                 ret ~= cast(ubyte) mac[j];
843         }
844 
845 /*
846 	string ret;
847 
848         for (int j = 0; j < mhash_get_block_size(algorithm); j++) {
849                 ret ~= std.string.format("%.2x", mac[j]);
850         }
851 */
852 
853 	return ret;
854 }
855 
856 pragma(lib, "mhash");