The OpenD Programming Language

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