├── .travis.yml ├── LICENSE ├── README.md ├── coverage.js ├── lib ├── basefacebook.js ├── cbutil.js ├── facebook.js ├── multipart.js └── requestutil.js ├── package.json ├── test ├── basefacebook.test.js ├── cbutil.test.js ├── facebook.test.js ├── lib │ └── testutil.js ├── multipart.test.js └── requestutil.test.js └── util ├── clean.bash └── create_signed_request.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.4 4 | - 0.6 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2011 Hitoshi Amano <seijro@gmail.com> 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Facebook Node SDK 2 | 3 | Facebook API Implementation in Node. 4 | 5 | [![Build Status](https://secure.travis-ci.org/amachang/facebook-node-sdk.png)](http://travis-ci.org/amachang/facebook-node-sdk) 6 | 7 | ## Features 8 | 9 | * Supports all Facebook Graph API, FQL, and REST API. 10 | * Compatible with the official Facebook PHP SDK. 11 | 12 | ## Install 13 | 14 | To install the most recent release from npm, run: 15 | 16 | npm install facebook-node-sdk 17 | 18 | ## Synopsis 19 | 20 | var Facebook = require('facebook-node-sdk'); 21 | 22 | var facebook = new Facebook({ appID: 'YOUR_APP_ID', secret: 'YOUR_APP_SECRET' }); 23 | 24 | facebook.api('/amachang', function(err, data) { 25 | console.log(data); // => { id: ... } 26 | }); 27 | 28 | ### With express framework (as connect middleware) 29 | 30 | var express = require('express'); 31 | var Facebook = require('facebook-node-sdk'); 32 | 33 | var app = express.createServer(); 34 | 35 | app.configure(function () { 36 | app.use(express.bodyParser()); 37 | app.use(express.cookieParser()); 38 | app.use(express.session({ secret: 'foo bar' })); 39 | app.use(Facebook.middleware({ appId: 'YOUR_APP_ID', secret: 'YOUR_APP_SECRET' })); 40 | }); 41 | 42 | app.get('/', Facebook.loginRequired(), function (req, res) { 43 | req.facebook.api('/me', function(err, user) { 44 | res.writeHead(200, {'Content-Type': 'text/plain'}); 45 | res.end('Hello, ' + user.name + '!'); 46 | }); 47 | }); 48 | 49 | -------------------------------------------------------------------------------- /coverage.js: -------------------------------------------------------------------------------- 1 | var spawn = require('child_process').spawn; 2 | 3 | var rm = spawn('rm', ['-rf', 'lib-cov']); 4 | rm.on('exit', function(code) { 5 | if (code !== 0) { 6 | console.error('Failure: rm -rf lib-cov'); 7 | return; 8 | } 9 | var jscov = spawn('./node_modules/.bin/node-jscoverage', ['lib', 'lib-cov']); 10 | jscov.on('exit', function(code) { 11 | if (code !== 0) { 12 | console.error('Failure: jscoverage'); 13 | return; 14 | } 15 | var expresso = spawn('./node_modules/.bin/expresso'); 16 | expresso.stdout.pipe(process.stdout); 17 | expresso.stderr.pipe(process.stderr); 18 | expresso.on('exit', function(code) { 19 | var rm = spawn('rm', ['-rf', 'lib-cov']).on('exit', function() { 20 | if (code !== 0) { 21 | consnole.log('Failure: expresso'); 22 | } 23 | }); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /lib/basefacebook.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var https = require('https'); 3 | var crypto = require('crypto'); 4 | var util = require('util'); 5 | var url = require('url'); 6 | var querystring = require('querystring'); 7 | var cb = require('./cbutil'); 8 | var requestUtil = require('./requestutil'); 9 | 10 | /** 11 | * Initialize a Facebook Application. 12 | * 13 | * The configuration: 14 | * - appId: the application ID 15 | * - secret: the application secret 16 | * - fileUpload: (optional) boolean indicating if file uploads are enabled 17 | * 18 | * @param array $config The application configuration 19 | */ 20 | function BaseFacebook(config) { 21 | if (config.hasOwnProperty('request')) { 22 | this.request = config.request; 23 | } 24 | if (config.hasOwnProperty('response')) { 25 | this.response = config.response; 26 | } 27 | if (config.hasOwnProperty('currentUrl')) { 28 | this.currentUrl = config.currentUrl; 29 | } 30 | this.setAppId(config.appId); 31 | this.setAppSecret(config.secret); 32 | if (config.hasOwnProperty('fileUpload')) { 33 | this.setFileUploadSupport(config.fileUpload); 34 | } 35 | 36 | var state = this.getPersistentData('state'); 37 | if (state) { 38 | this.state = state; 39 | } 40 | this.accessToken = this.getPersistentData('access_token', null); 41 | this.code = this.getPersistentData('code', null); 42 | } 43 | 44 | BaseFacebook.prototype.request = null; 45 | BaseFacebook.prototype.response = null; 46 | 47 | BaseFacebook.prototype.currentUrl = null; 48 | 49 | BaseFacebook.prototype.sessionNameMap = { 50 | access_token: 'access_token', 51 | user_id: 'user_id', 52 | code: 'code', 53 | state: 'state' 54 | }; 55 | 56 | BaseFacebook.prototype.getRequestParam = function(key) { 57 | if (!this.request) { 58 | return null; 59 | } 60 | if (this.request.query && this.request.query.hasOwnProperty(key)) { 61 | return this.request.query[key]; 62 | } 63 | else if (this.request.body && this.request.body.hasOwnProperty(key)) { 64 | return this.request.body[key]; 65 | } 66 | else { 67 | return null; 68 | } 69 | }; 70 | 71 | BaseFacebook.prototype.getCookie = function(key) { 72 | if (this.hasCookie(key)) { 73 | return this.request.cookies[key]; 74 | } 75 | else { 76 | return null; 77 | } 78 | }; 79 | 80 | BaseFacebook.prototype.hasCookie = function(key) { 81 | return this.request && this.request.cookies && this.request.cookies.hasOwnProperty(key); 82 | }; 83 | 84 | BaseFacebook.prototype.sentHeaders = function() { 85 | return this.response && this.response._header; 86 | }; 87 | 88 | BaseFacebook.prototype.clearCookie = function(key, options) { 89 | if (this.response) { 90 | this.response.clearCookie.apply(this.response, arguments); 91 | } 92 | }; 93 | 94 | /* 95 | We don't need yet. 96 | BaseFacebook.prototype.setCookie = function(key, value, options) { 97 | if (this.response) { 98 | this.response.cookie.apply(this.response, arguments); 99 | } 100 | }; 101 | */ 102 | 103 | BaseFacebook.prototype.appId = null; 104 | 105 | /** 106 | * Set the Application ID. 107 | * 108 | * @param string appId The Application ID 109 | * @return BaseFacebook 110 | */ 111 | BaseFacebook.prototype.setAppId = function(appId) { 112 | this.appId = appId; 113 | return this; 114 | }; 115 | 116 | /** 117 | * Get the Application ID. 118 | * 119 | * @return string the Application ID 120 | */ 121 | BaseFacebook.prototype.getAppId = function() { 122 | return this.appId; 123 | }; 124 | 125 | BaseFacebook.prototype.appSecret = null; 126 | 127 | /** 128 | * Set the App Secret. 129 | * 130 | * @param string appSecret The App Secret 131 | * @return BaseFacebook 132 | * @deprecated 133 | */ 134 | BaseFacebook.prototype.setApiSecret = function(appSecret) { 135 | this.appSecret = appSecret; 136 | return this; 137 | }; 138 | 139 | /** 140 | * Set the App Secret. 141 | * 142 | * @param string appSecret The App Secret 143 | * @return BaseFacebook 144 | */ 145 | BaseFacebook.prototype.setAppSecret = function(appSecret) { 146 | this.appSecret = appSecret; 147 | return this; 148 | }; 149 | 150 | /** 151 | * Get the App Secret. 152 | * 153 | * @return string the App Secret 154 | * @deprecated 155 | */ 156 | BaseFacebook.prototype.getApiSecret = function() { 157 | return this.appSecret; 158 | }; 159 | 160 | /** 161 | * Get the App Secret. 162 | * 163 | * @return string the App Secret 164 | */ 165 | BaseFacebook.prototype.getAppSecret = function() { 166 | return this.appSecret; 167 | }; 168 | 169 | BaseFacebook.prototype.fileUploadSupport = false; 170 | 171 | /** 172 | * Set the file upload support status. 173 | * 174 | * @param boolean $fileUploadSupport The file upload support status. 175 | * @return BaseFacebook 176 | */ 177 | BaseFacebook.prototype.setFileUploadSupport = function(fileUploadSupport) { 178 | this.fileUploadSupport = fileUploadSupport; 179 | return this; 180 | }; 181 | 182 | /** 183 | * Get the file upload support status. 184 | * 185 | * @return boolean true if and only if the server supports file upload. 186 | */ 187 | BaseFacebook.prototype.getFileUploadSupport = function() { 188 | return this.fileUploadSupport; 189 | }; 190 | 191 | 192 | /** 193 | * DEPRECATED! Please use getFileUploadSupport instead. 194 | * 195 | * Get the file upload support status. 196 | * 197 | * @return boolean true if and only if the server supports file upload. 198 | */ 199 | BaseFacebook.prototype.useFileUploadSupport = function() { 200 | return this.getFileUploadSupport(); 201 | } 202 | 203 | 204 | BaseFacebook.prototype.accessToken = null; 205 | 206 | /** 207 | * Sets the access token for api calls. Use this if you get 208 | * your access token by other means and just want the SDK 209 | * to use it. 210 | * 211 | * @param string $access_token an access token. 212 | * @return BaseFacebook 213 | */ 214 | BaseFacebook.prototype.setAccessToken = function(accessToken) { 215 | this.accessToken = accessToken; 216 | return this; 217 | }; 218 | 219 | /** 220 | * Determines the access token that should be used for API calls. 221 | * The first time this is called, $this->accessToken is set equal 222 | * to either a valid user access token, or it's set to the application 223 | * access token if a valid user access token wasn't available. Subsequent 224 | * calls return whatever the first call returned. 225 | * 226 | * @return string The access token 227 | */ 228 | BaseFacebook.prototype.getAccessToken = function getAccessToken(callback) { 229 | if (this.accessToken !== null) { 230 | // we've done this already and cached it. Just return. 231 | callback(null, this.accessToken); 232 | } 233 | else { 234 | // first establish access token to be the application 235 | // access token, in case we navigate to the /oauth/access_token 236 | // endpoint, where SOME access token is required. 237 | this.setAccessToken(this.getApplicationAccessToken()); 238 | var self = this; 239 | this.getUserAccessToken(cb.returnToCallback(callback, false, function(userAccessToken) { 240 | if (userAccessToken) { 241 | self.setAccessToken(userAccessToken); 242 | } 243 | return self.accessToken; 244 | })); 245 | } 246 | }; 247 | 248 | BaseFacebook.prototype.getAccessToken = cb.wrap(BaseFacebook.prototype.getAccessToken); 249 | 250 | /** 251 | * Determines and returns the user access token, first using 252 | * the signed request if present, and then falling back on 253 | * the authorization code if present. The intent is to 254 | * return a valid user access token, or false if one is determined 255 | * to not be available. 256 | * 257 | * @return string A valid user access token, or false if one 258 | * could not be determined. 259 | */ 260 | BaseFacebook.prototype.getUserAccessToken = function getUserAccessToken(callback) { 261 | // first, consider a signed request if it's supplied. 262 | // if there is a signed request, then it alone determines 263 | // the access token. 264 | var signedRequest = this.getSignedRequest(); 265 | if (signedRequest) { 266 | // apps.facebook.com hands the access_token in the signed_request 267 | if (signedRequest.hasOwnProperty('oauth_token')) { 268 | var accessToken = signedRequest.oauth_token; 269 | this.setPersistentData('access_token', accessToken); 270 | callback(null, accessToken); 271 | } 272 | else { 273 | // the JS SDK puts a code in with the redirect_uri of '' 274 | if (signedRequest.hasOwnProperty('code')) { 275 | var code = signedRequest.code; 276 | var self = this; 277 | this.getAccessTokenFromCode(code, '', cb.returnToCallback(callback, false, handleAccessTokenFromCode)); 278 | } 279 | else { 280 | // signed request states there's no access token, so anything 281 | // stored should be cleared. 282 | this.clearAllPersistentData(); 283 | // respect the signed request's data, even 284 | // if there's an authorization code or something else 285 | callback(null, false); 286 | } 287 | } 288 | } 289 | else { 290 | var code = this.getCode(); 291 | if (code && code !== this.getPersistentData('code')) { 292 | var self = this; 293 | this.getAccessTokenFromCode(code, null, cb.returnToCallback(callback, false, handleAccessTokenFromCode)); 294 | } 295 | else { 296 | // as a fallback, just return whatever is in the persistent 297 | // store, knowing nothing explicit (signed request, authorization 298 | // code, etc.) was present to shadow it (or we saw a code in $_REQUEST, 299 | // but it's the same as what's in the persistent store) 300 | callback(null, this.getPersistentData('access_token')); 301 | } 302 | } 303 | function handleAccessTokenFromCode(accessToken) { 304 | if (accessToken) { 305 | self.setPersistentData('code', code); 306 | self.setPersistentData('access_token', accessToken); 307 | return accessToken; 308 | } 309 | else { 310 | // signed request states there's no access token, so anything 311 | // stored should be cleared. 312 | self.clearAllPersistentData(); 313 | // respect the signed request's data, even 314 | // if there's an authorization code or something else 315 | return false; 316 | } 317 | } 318 | }; 319 | 320 | BaseFacebook.prototype.getUserAccessToken = cb.wrap(BaseFacebook.prototype.getUserAccessToken); 321 | 322 | BaseFacebook.prototype.mergeObject = function() { 323 | var obj = {}; 324 | for (var i = 0; i < arguments.length; i++) { 325 | var arg = arguments[i]; 326 | for (var name in arg) { 327 | if (arg.hasOwnProperty(name)) { 328 | obj[name] = arg[name]; 329 | } 330 | } 331 | } 332 | return obj; 333 | }; 334 | 335 | /** 336 | * Get a Login URL for use with redirects. By default, full page redirect is 337 | * assumed. If you are using the generated URL with a window.open() call in 338 | * JavaScript, you can pass in display=popup as part of the $params. 339 | * 340 | * The parameters: 341 | * - redirect_uri: the url to go to after a successful login 342 | * - scope: comma separated list of requested extended perms 343 | * 344 | * @param array $params Provide custom parameters 345 | * @return string The URL for the login flow 346 | * 347 | * @throws Error 348 | */ 349 | BaseFacebook.prototype.getLoginUrl = function(params) { 350 | if (!params) { 351 | params = {}; 352 | } 353 | this.establishCSRFTokenState(); 354 | var currentUrl = this.getCurrentUrl(); 355 | 356 | // if 'scope' is passed as an array, convert to comma separated list 357 | var scopeParams = params.hasOwnProperty('scope') ? params.scope : null; 358 | if (scopeParams && isArray(scopeParams)) { 359 | params.scope = scopeParams.join(','); 360 | } 361 | 362 | return 'https://www.facebook.com/dialog/oauth?' + querystring.stringify(this.mergeObject({ 363 | client_id: this.getAppId(), 364 | redirect_uri: currentUrl, // possibly overwritten 365 | state: this.state 366 | }, params)); 367 | }; 368 | 369 | /** 370 | * Get a Logout URL suitable for use with redirects. 371 | * 372 | * The parameters: 373 | * - next: the url to go to after a successful logout 374 | * 375 | * @param array $params Provide custom parameters 376 | * @return string The URL for the logout flow 377 | */ 378 | BaseFacebook.prototype.getLogoutUrl = function getLogoutUrl(/* params, callback */) { 379 | var args = [].slice.call(arguments); 380 | var callback = args.pop(); 381 | var params = args.shift(); 382 | if (!params) { 383 | params = {}; 384 | } 385 | 386 | var self = this; 387 | this.getAccessToken(cb.returnToCallback(callback, false, function(accessToken) { 388 | var currentUrl = self.getCurrentUrl(); 389 | var queryMap = self.mergeObject({ next: currentUrl, access_token: accessToken }, params); 390 | var query = querystring.stringify(queryMap) 391 | return 'https://www.facebook.com/logout.php?' + query; 392 | })); 393 | }; 394 | 395 | BaseFacebook.prototype.getLogoutUrl = cb.wrap(BaseFacebook.prototype.getLogoutUrl); 396 | 397 | /** 398 | * Get a login status URL to fetch the status from Facebook. 399 | * 400 | * The parameters: 401 | * - ok_session: the URL to go to if a session is found 402 | * - no_session: the URL to go to if the user is not connected 403 | * - no_user: the URL to go to if the user is not signed into facebook 404 | * 405 | * @param array $params Provide custom parameters 406 | * @return string The URL for the logout flow 407 | * 408 | * @throws Error 409 | */ 410 | BaseFacebook.prototype.getLoginStatusUrl = function(params) { 411 | var currentUrl = this.getCurrentUrl(); 412 | return 'https://www.facebook.com/extern/login_status.php?' + querystring.stringify(this.mergeObject({ 413 | api_key: this.getAppId(), 414 | no_session: currentUrl, 415 | no_user: currentUrl, 416 | ok_session: currentUrl, 417 | session_version: 3 418 | }, params)); 419 | }; 420 | 421 | BaseFacebook.prototype.state = null; 422 | 423 | /** 424 | * Lays down a CSRF state token for this process. 425 | * 426 | * @return void 427 | */ 428 | BaseFacebook.prototype.establishCSRFTokenState = function() { 429 | if (this.state === null) { 430 | var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 431 | var buf = []; 432 | for (var i = 0; i < 32; i++) { 433 | buf.push(chars[Math.floor(chars.length * Math.random())]); 434 | } 435 | this.state = buf.join(''); 436 | this.setPersistentData('state', this.state); 437 | } 438 | }; 439 | 440 | /** 441 | * Returns the access token that should be used for logged out 442 | * users when no authorization code is available. 443 | * 444 | * @return string The application access token, useful for gathering 445 | * public information about users and applications. 446 | */ 447 | BaseFacebook.prototype.getApplicationAccessToken = function() { 448 | return this.appId + '|' + this.appSecret; 449 | } 450 | 451 | BaseFacebook.prototype.signedRequest = null; 452 | 453 | /** 454 | * Retrieve the signed request, either from a request parameter or, 455 | * if not present, from a cookie. 456 | * 457 | * @return string the signed request, if available, or null otherwise. 458 | */ 459 | BaseFacebook.prototype.getSignedRequest = function() { 460 | if (this.signedRequest === null) { 461 | var param = this.getRequestParam('signed_request'); 462 | if (param !== null) { 463 | parseSignedRequestWrapped(this, param); 464 | } 465 | if (this.signedRequest === null) { 466 | var cookieName = this.getSignedRequestCookieName(); 467 | var cookieValue = this.getCookie(cookieName); 468 | if (cookieValue !== null) { 469 | parseSignedRequestWrapped(this, cookieValue); 470 | } 471 | } 472 | } 473 | 474 | function parseSignedRequestWrapped(self, str) { 475 | try { 476 | self.signedRequest = self.parseSignedRequest(str); 477 | } 478 | catch (err) { 479 | self.signedRequest = null; 480 | self.errorLog('Parse error sigened_request cookie: ' + str); 481 | } 482 | } 483 | 484 | return this.signedRequest; 485 | }; 486 | 487 | BaseFacebook.prototype.user = null; 488 | 489 | /** 490 | * Get the UID of the connected user, or 0 491 | * if the Facebook user is not connected. 492 | * 493 | * @return string the UID if available. 494 | */ 495 | BaseFacebook.prototype.getUser = function getUser(callback) { 496 | if (this.user !== null) { 497 | // we've already determined this and cached the value. 498 | callback(null, this.user); 499 | } 500 | else { 501 | var self = this; 502 | this.getUserFromAvailableData(cb.returnToCallback(callback, false, function(user) { 503 | self.user = user; 504 | return self.user; 505 | })); 506 | } 507 | }; 508 | 509 | BaseFacebook.prototype.getUser = cb.wrap(BaseFacebook.prototype.getUser); 510 | 511 | /** 512 | * Determines the connected user by first examining any signed 513 | * requests, then considering an authorization code, and then 514 | * falling back to any persistent store storing the user. 515 | * 516 | * @return integer The id of the connected Facebook user, 517 | * or 0 if no such user exists. 518 | */ 519 | BaseFacebook.prototype.getUserFromAvailableData = function getUserFromAvailableData(callback) { 520 | // if a signed request is supplied, then it solely determines 521 | // who the user is. 522 | var signedRequest = this.getSignedRequest(); 523 | if (signedRequest) { 524 | if (signedRequest.hasOwnProperty('user_id')) { 525 | var user = signedRequest.user_id; 526 | assert.ok(typeof user === 'string' && user.match(/^\d+$/)); 527 | this.setPersistentData('user_id', user); 528 | callback(null, user); 529 | } 530 | else { 531 | // if the signed request didn't present a user id, then invalidate 532 | // all entries in any persistent store. 533 | this.clearAllPersistentData(); 534 | callback(null, 0); 535 | } 536 | } 537 | else { 538 | var user = this.getPersistentData('user_id', 0); 539 | var persistedAccessToken = this.getPersistentData('access_token'); 540 | // use access_token to fetch user id if we have a user access_token, or if 541 | // the cached access token has changed. 542 | var self = this; 543 | this.getAccessToken(function(err, accessToken) { 544 | try { 545 | if (err) { 546 | throw err; 547 | } 548 | if ((accessToken) && 549 | // access_token is not application access_token 550 | (accessToken !== self.getApplicationAccessToken()) && 551 | // undefined user or access_token is old 552 | (!user || persistedAccessToken !== accessToken)) { 553 | 554 | self.getUserFromAccessToken(cb.returnToCallback(callback, false, function(user) { 555 | if (user) { 556 | assert.ok(typeof user === 'string' && user.match(/^\d+$/)); 557 | self.setPersistentData('user_id', user); 558 | } 559 | else { 560 | self.clearAllPersistentData(); 561 | } 562 | return user; 563 | })); 564 | } 565 | else { 566 | callback(null, user); 567 | } 568 | } 569 | catch (err) { 570 | callback(err, null); 571 | } 572 | }); 573 | } 574 | }; 575 | 576 | BaseFacebook.prototype.getUserFromAvailableData = cb.wrap(BaseFacebook.prototype.getUserFromAvailableData); 577 | 578 | /** 579 | * Make an API call. 580 | * 581 | * @return mixed The decoded response 582 | */ 583 | BaseFacebook.prototype.api = function api(/* polymorphic */) { 584 | var args = [].slice.call(arguments); 585 | if (args[0] && typeof args[0] === 'object') { 586 | var callback = args.pop(); 587 | this.restserver(args[0], callback); 588 | } else { 589 | this.graph.apply(this, args); 590 | } 591 | }; 592 | 593 | BaseFacebook.prototype.api = cb.wrap(BaseFacebook.prototype.api); 594 | 595 | /** 596 | * Invoke the old restserver.php endpoint. 597 | * 598 | * @param array $params Method call object 599 | * 600 | * @return mixed The decoded response object 601 | */ 602 | BaseFacebook.prototype.restserver = function restserver(params, callback) { 603 | // generic application level parameters 604 | params.api_key = this.getAppId(); 605 | params.format = 'json-strings'; 606 | 607 | var self = this; 608 | var host = this.getApiHost(params['method']) 609 | this.oauthRequest(host, '/restserver.php', params, cb.returnToCallback(callback, false, function(response) { 610 | try { 611 | var result = JSON.parse(response); 612 | } 613 | catch (err) { 614 | throw new Error('Parse REST server response error: ' + err.message); 615 | } 616 | // results are returned, errors are thrown 617 | if (result && typeof result === 'object' && result.hasOwnProperty('error_code')) { 618 | throw self.createApiError(result); 619 | } 620 | else { 621 | if (params.method === 'auth.expireSession' || params.method === 'auth.revokeAuthorization') { 622 | self.destroySession(); 623 | } 624 | return result; 625 | } 626 | })); 627 | }; 628 | 629 | BaseFacebook.prototype.restserver = cb.wrap(BaseFacebook.prototype.restserver); 630 | 631 | /** 632 | * Return true if this is video post. 633 | * 634 | * @param string $path The path 635 | * @param string $method The http method (default 'GET') 636 | * 637 | * @return boolean true if this is video post 638 | */ 639 | BaseFacebook.prototype.isVideoPost = function isVideoPost(path, method) { 640 | method = method || 'GET'; 641 | if (method == 'POST' && path.match(/^(\/)(.+)(\/)(videos)$/)) { 642 | return true; 643 | } 644 | return false; 645 | } 646 | 647 | /** 648 | * Invoke the Graph API. 649 | * 650 | * @param string $path The path (required) 651 | * @param string $method The http method (default 'GET') 652 | * @param array $params The query/post data 653 | * 654 | * @return mixed The decoded response object 655 | */ 656 | BaseFacebook.prototype.graph = function graph(/* path, method, params, callback */) { 657 | var args = [].slice.call(arguments); 658 | var callback = args.pop(); 659 | var path = args.shift(); 660 | var method = 'GET'; 661 | var params = {}; 662 | if (args.length === 1) { 663 | if (typeof args[0] === 'string') { 664 | method = args[0]; 665 | } 666 | else { 667 | params = args[0]; 668 | } 669 | } 670 | else if (args.length === 2){ 671 | method = args[0]; 672 | params = args[1]; 673 | } 674 | 675 | params.method = method; // method override as we always do a POST 676 | 677 | var domain = this.isVideoPost(path, method) ? 'graph-video.facebook.com' : 'graph.facebook.com'; 678 | 679 | var self = this; 680 | this.oauthRequest(domain, path, params, cb.returnToCallback(callback, false, function(response) { 681 | try { 682 | var result = JSON.parse(response); 683 | } 684 | catch (err) { 685 | throw new Error('Parse Graph API server response error: ' + err.message); 686 | } 687 | if (result && typeof result === 'object' && result.hasOwnProperty('error')) { 688 | throw self.createApiError(result); 689 | } 690 | else { 691 | return result; 692 | } 693 | })); 694 | }; 695 | 696 | BaseFacebook.prototype.graph = cb.wrap(BaseFacebook.prototype.graph); 697 | 698 | /** 699 | * Analyzes the supplied result to see if it was thrown 700 | * because the access token is no longer valid. If that is 701 | * the case, then we destroy the session. 702 | * 703 | * @param $result array A record storing the error message returned 704 | * by a failed API call. 705 | */ 706 | BaseFacebook.prototype.createApiError = function(result) { 707 | var err = new FacebookApiError(result); 708 | switch (err.getType()) { 709 | // OAuth 2.0 Draft 00 style 710 | case 'OAuthException': 711 | // OAuth 2.0 Draft 10 style 712 | case 'invalid_token': 713 | // REST server errors are just Exceptions 714 | case 'Exception': 715 | var message = err.message; 716 | if ((message.indexOf('Error validating access token') !== -1) || 717 | (message.indexOf('Invalid OAuth access token') !== -1) || 718 | (message.indexOf('An active access token must be used') !== -1)) { 719 | this.destroySession(); 720 | } 721 | break; 722 | } 723 | return err; 724 | }; 725 | 726 | /** 727 | * Destroy the current session 728 | */ 729 | BaseFacebook.prototype.destroySession = function() { 730 | this.accessToken = null; 731 | this.signedRequest = null; 732 | this.user = null; 733 | this.clearAllPersistentData(); 734 | 735 | if (this.request) { 736 | // Javascript sets a cookie that will be used in getSignedRequest that we 737 | // need to clear if we can 738 | var cookieName = this.getSignedRequestCookieName(); 739 | 740 | if (this.hasCookie(cookieName)) { 741 | if (this.request.cookies) { 742 | delete this.request.cookies[cookieName]; 743 | } 744 | 745 | if (this.response) { 746 | if (!this.sentHeaders()) { 747 | // The base domain is stored in the metadata cookie if not we fallback 748 | // to the current hostname 749 | var host = this.request.headers['x-forwarded-host'] || this.request.headers.host; 750 | var baseDomain = '.' + host; 751 | 752 | var metadata = this.getMetadataCookie(); 753 | if (metadata.hasOwnProperty('base_domain') && typeof metadata['base_domain'] === 'string' && metadata['base_domain'] !== '') { 754 | baseDomain = metadata['base_domain']; 755 | } 756 | 757 | this.clearCookie(cookieName, { path: '/', domain: baseDomain }); 758 | } 759 | else { 760 | this.errorLog( 761 | 'There exists a cookie that we wanted to clear that we couldn\'t ' + 762 | 'clear because headers was already sent. Make sure to do the first ' + 763 | 'API call before outputing anything' 764 | ); 765 | } 766 | } 767 | } 768 | } 769 | }; 770 | 771 | /** 772 | * Parses the metadata cookie that our Javascript API set 773 | * 774 | * @return an array mapping key to value 775 | */ 776 | BaseFacebook.prototype.getMetadataCookie = function getMetadataCookie() { 777 | var cookieName = this.getMetadataCookieName(); 778 | if (!this.hasCookie(cookieName)) { 779 | return {}; 780 | } 781 | 782 | // The cookie value can be wrapped in "-characters so remove them 783 | var cookieValue = this.getCookie(cookieName); 784 | cookieValue = cookieValue.replace(/"/g, ''); 785 | 786 | if (cookieValue === '') { 787 | return {}; 788 | } 789 | 790 | var parts = cookieValue.split(/&/); 791 | var metadata = {}; 792 | for (var i = 0; i < parts.length; i++) { 793 | var part = parts[i]; 794 | var pair = part.split(/=/, 2); 795 | if (pair[0] !== '') { 796 | metadata[decodeURIComponent(pair[0])] = (pair.length > 1) ? decodeURIComponent(pair[1]) : ''; 797 | } 798 | } 799 | 800 | return metadata; 801 | }; 802 | 803 | BaseFacebook.prototype.apiReadOnlyCalls = { 804 | 'admin.getallocation': true, 805 | 'admin.getappproperties': true, 806 | 'admin.getbannedusers': true, 807 | 'admin.getlivestreamvialink': true, 808 | 'admin.getmetrics': true, 809 | 'admin.getrestrictioninfo': true, 810 | 'application.getpublicinfo': true, 811 | 'auth.getapppublickey': true, 812 | 'auth.getsession': true, 813 | 'auth.getsignedpublicsessiondata': true, 814 | 'comments.get': true, 815 | 'connect.getunconnectedfriendscount': true, 816 | 'dashboard.getactivity': true, 817 | 'dashboard.getcount': true, 818 | 'dashboard.getglobalnews': true, 819 | 'dashboard.getnews': true, 820 | 'dashboard.multigetcount': true, 821 | 'dashboard.multigetnews': true, 822 | 'data.getcookies': true, 823 | 'events.get': true, 824 | 'events.getmembers': true, 825 | 'fbml.getcustomtags': true, 826 | 'feed.getappfriendstories': true, 827 | 'feed.getregisteredtemplatebundlebyid': true, 828 | 'feed.getregisteredtemplatebundles': true, 829 | 'fql.multiquery': true, 830 | 'fql.query': true, 831 | 'friends.arefriends': true, 832 | 'friends.get': true, 833 | 'friends.getappusers': true, 834 | 'friends.getlists': true, 835 | 'friends.getmutualfriends': true, 836 | 'gifts.get': true, 837 | 'groups.get': true, 838 | 'groups.getmembers': true, 839 | 'intl.gettranslations': true, 840 | 'links.get': true, 841 | 'notes.get': true, 842 | 'notifications.get': true, 843 | 'pages.getinfo': true, 844 | 'pages.isadmin': true, 845 | 'pages.isappadded': true, 846 | 'pages.isfan': true, 847 | 'permissions.checkavailableapiaccess': true, 848 | 'permissions.checkgrantedapiaccess': true, 849 | 'photos.get': true, 850 | 'photos.getalbums': true, 851 | 'photos.gettags': true, 852 | 'profile.getinfo': true, 853 | 'profile.getinfooptions': true, 854 | 'stream.get': true, 855 | 'stream.getcomments': true, 856 | 'stream.getfilters': true, 857 | 'users.getinfo': true, 858 | 'users.getloggedinuser': true, 859 | 'users.getstandardinfo': true, 860 | 'users.hasapppermission': true, 861 | 'users.isappuser': true, 862 | 'users.isverified': true, 863 | 'video.getuploadlimits': true 864 | }; 865 | 866 | /** 867 | * Build the URL for api given parameters. 868 | * 869 | * @param $method String the method name. 870 | * @return string The URL for the given parameters 871 | */ 872 | BaseFacebook.prototype.getApiHost = function(method) { 873 | var host = 'api.facebook.com'; 874 | if (this.apiReadOnlyCalls.hasOwnProperty(method.toLowerCase())) { 875 | host = 'api-read.facebook.com'; 876 | } 877 | else if (method.toLowerCase() === 'video.upload') { 878 | host = 'api-video.facebook.com'; 879 | } 880 | return host; 881 | }; 882 | 883 | /** 884 | * Constructs and returns the name of the cookie that 885 | * potentially houses the signed request for the app user. 886 | * The cookie is not set by the BaseFacebook class, but 887 | * it may be set by the JavaScript SDK. 888 | * 889 | * @return string the name of the cookie that would house 890 | * the signed request value. 891 | */ 892 | BaseFacebook.prototype.getSignedRequestCookieName = function() { 893 | return 'fbsr_' + this.getAppId(); 894 | }; 895 | 896 | /** 897 | * Parses a signed_request and validates the signature. 898 | * 899 | * @param string $signed_request A signed token 900 | * @return array The payload inside it or null if the sig is wrong 901 | * 902 | * @throws Error 903 | */ 904 | BaseFacebook.prototype.parseSignedRequest = function(signedRequest) { 905 | var splittedSignedRequest = signedRequest.split(/\./); 906 | var encodedSig = splittedSignedRequest.shift(); 907 | var payload = splittedSignedRequest.join('.'); 908 | 909 | // decode the data 910 | var sig = this.base64UrlDecode(encodedSig); 911 | 912 | // must catch in caller 913 | var data = JSON.parse(this.base64UrlDecode(payload).toString('utf8')); 914 | 915 | if (data.algorithm.toUpperCase() !== 'HMAC-SHA256') { 916 | this.errorLog('Unknown algorithm. Expected HMAC-SHA256'); 917 | return null; 918 | } 919 | 920 | // check sig 921 | var hmac = crypto.createHmac('sha256', this.getAppSecret()); 922 | hmac.update(payload); 923 | var expectedSig = hmac.digest('base64'); 924 | if (sig.toString('base64') !== expectedSig) { 925 | this.errorLog('Bad Signed JSON signature!'); 926 | return null; 927 | } 928 | 929 | return data; 930 | }; 931 | 932 | BaseFacebook.prototype.base64UrlDecode = function(input) { 933 | var base64 = input.replace(/-/g, '+').replace(/_/g, '/'); 934 | return new Buffer(base64, 'base64'); 935 | }; 936 | 937 | /** 938 | * Constructs and returns the name of the coookie that potentially contain 939 | * metadata. The cookie is not set by the BaseFacebook class, but it may be 940 | * set by the JavaScript SDK. 941 | * 942 | * @return string the name of the cookie that would house metadata. 943 | */ 944 | BaseFacebook.prototype.getMetadataCookieName = function getMetadataCookieName() { 945 | return 'fbm_' + this.getAppId(); 946 | }; 947 | 948 | /** 949 | * Get the authorization code from the query parameters, if it exists, 950 | * and otherwise return false to signal no authorization code was 951 | * discoverable. 952 | * 953 | * @return mixed The authorization code, or false if the authorization 954 | * code could not be determined. 955 | */ 956 | BaseFacebook.prototype.getCode = function() { 957 | var code = this.getRequestParam('code'); 958 | if (code !== null) { 959 | var state = this.getRequestParam('state'); 960 | if (this.state !== null && state !== null && this.state === state) { 961 | 962 | // CSRF state has done its job, so clear it 963 | this.state = null; 964 | this.clearPersistentData('state'); 965 | return code; 966 | } else { 967 | this.errorLog('CSRF state token does not match one provided.'); 968 | return false; 969 | } 970 | } 971 | 972 | return false; 973 | }; 974 | 975 | /** 976 | * Retrieves the UID with the understanding that 977 | * $this->accessToken has already been set and is 978 | * seemingly legitimate. It relies on Facebook's Graph API 979 | * to retrieve user information and then extract 980 | * the user ID. 981 | * 982 | * @return integer Returns the UID of the Facebook user, or 0 983 | * if the Facebook user could not be determined. 984 | */ 985 | BaseFacebook.prototype.getUserFromAccessToken = function getUserFromAccessToken(callback) { 986 | this.api('/me', cb.returnToCallback(callback, true, function(err, userInfo) { 987 | if (err) { 988 | return 0; 989 | } 990 | else { 991 | return userInfo.id; 992 | } 993 | })); 994 | }; 995 | 996 | BaseFacebook.prototype.getUserFromAccessToken = cb.wrap(BaseFacebook.prototype.getUserFromAccessToken); 997 | 998 | /** 999 | * Retrieves an access token for the given authorization code 1000 | * (previously generated from www.facebook.com on behalf of 1001 | * a specific user). The authorization code is sent to graph.facebook.com 1002 | * and a legitimate access token is generated provided the access token 1003 | * and the user for which it was generated all match, and the user is 1004 | * either logged in to Facebook or has granted an offline access permission. 1005 | * 1006 | * @param string $code An authorization code. 1007 | * @return mixed An access token exchanged for the authorization code, or 1008 | * false if an access token could not be generated. 1009 | */ 1010 | BaseFacebook.prototype.getAccessTokenFromCode = function getAccessTokenFromCode(code, redirectUri, callback) { 1011 | if (!code) { 1012 | callback(null, false); 1013 | } 1014 | else { 1015 | if (redirectUri === null || redirectUri === undefined) { 1016 | redirectUri = this.getCurrentUrl(); 1017 | } 1018 | 1019 | // need to circumvent json_decode by calling oauthRequest 1020 | // directly, since response isn't JSON format. 1021 | this.oauthRequest('graph.facebook.com', '/oauth/access_token', { 1022 | client_id: this.getAppId(), 1023 | client_secret: this.getAppSecret(), 1024 | redirect_uri: redirectUri, 1025 | code: code 1026 | }, 1027 | cb.returnToCallback(callback, true, function(err, accessTokenResponse) { 1028 | if (err) { 1029 | if (err instanceof FacebookApiError) { 1030 | // most likely that user very recently revoked authorization. 1031 | // In any event, we don't have an access token, so say so. 1032 | return false; 1033 | } 1034 | else { 1035 | throw err; 1036 | } 1037 | } 1038 | else { 1039 | if (!accessTokenResponse) { 1040 | return false; 1041 | } 1042 | else { 1043 | var responseParams = querystring.parse(accessTokenResponse); 1044 | if (!responseParams.hasOwnProperty('access_token')) { 1045 | return false; 1046 | } 1047 | else { 1048 | return responseParams.access_token; 1049 | } 1050 | } 1051 | } 1052 | })); 1053 | } 1054 | }; 1055 | 1056 | BaseFacebook.prototype.getAccessTokenFromCode = cb.wrap(BaseFacebook.prototype.getAccessTokenFromCode); 1057 | 1058 | /** 1059 | * Returns the Current URL, stripping it of known FB parameters that should 1060 | * not persist. 1061 | * 1062 | * @return string The current URL 1063 | * @throws Errror 1064 | */ 1065 | BaseFacebook.prototype.getCurrentUrl = function() { 1066 | if (this.currentUrl !== null) { 1067 | return this.currentUrl; 1068 | } 1069 | 1070 | if (!this.request) { 1071 | throw new Error('No request object.'); 1072 | } 1073 | 1074 | var req = this.request; 1075 | var conn = req.connection; 1076 | var headers = req.headers; 1077 | if (conn.pair || req.https === 'on' || headers['x-forwarded-proto'] === 'https') { 1078 | var protocol = 'https://'; 1079 | } 1080 | else { 1081 | var protocol = 'http://'; 1082 | } 1083 | 1084 | var host = headers['x-forwarded-host'] || headers.host; 1085 | var path = req.url; 1086 | 1087 | var currentUrl = protocol + host + path; 1088 | 1089 | var parts = url.parse(currentUrl); 1090 | 1091 | if (parts.query) { 1092 | var params = parts.query.split(/&/); 1093 | var self = this; 1094 | delete parts.href; 1095 | delete parts.path; 1096 | delete parts.query; 1097 | 1098 | parts.search = ''; 1099 | 1100 | params = params.filter(function(param) { return self.shouldRetainParam(param) }); 1101 | if (params.length > 0) { 1102 | parts.search = '?' + params.join('&'); 1103 | } 1104 | } 1105 | 1106 | return url.format(parts); 1107 | }; 1108 | 1109 | /** 1110 | * Returns true if and only if the key or key/value pair should 1111 | * be retained as part of the query string. This amounts to 1112 | * a brute-force search of the very small list of Facebook-specific 1113 | * params that should be stripped out. 1114 | * 1115 | * @param string $param A key or key/value pair within a URL's query (e.g. 1116 | * 'foo=a', 'foo=', or 'foo'. 1117 | * 1118 | * @return boolean 1119 | */ 1120 | BaseFacebook.prototype.shouldRetainParam = function(param) { 1121 | var splited = param.split(/=/); 1122 | return !((splited.length > 1) && this.dropQueryParams.hasOwnProperty(splited[0])); 1123 | }; 1124 | 1125 | BaseFacebook.prototype.dropQueryParams = { 1126 | code: true, 1127 | state: true, 1128 | signed_request: true, 1129 | base_domain: true 1130 | }; 1131 | 1132 | /** 1133 | * Make a OAuth Request. 1134 | * 1135 | * @param string $url The path (required) 1136 | * @param array $params The query/post data 1137 | * 1138 | * @return string The decoded response object 1139 | */ 1140 | BaseFacebook.prototype.oauthRequest = function oauthRequest(host, path, params, callback) { 1141 | var self = this; 1142 | if (!params.hasOwnProperty('access_token')) { 1143 | this.getAccessToken(function(err, accessToken) { 1144 | try { 1145 | if (err) { 1146 | throw err; 1147 | } 1148 | params['access_token'] = accessToken; 1149 | next(); 1150 | } 1151 | catch (err) { 1152 | callback(err, null); 1153 | } 1154 | }); 1155 | } 1156 | else { 1157 | next(); 1158 | } 1159 | function next() { 1160 | // json_encode all params values that are not strings 1161 | for (var key in params) { 1162 | var value = params[key]; 1163 | if (typeof value !== 'string') { 1164 | params[key] = JSON.stringify(value); 1165 | } 1166 | } 1167 | 1168 | self.makeRequest(host, path, params, callback); 1169 | } 1170 | }; 1171 | 1172 | BaseFacebook.prototype.oauthRequest = cb.wrap(BaseFacebook.prototype.oauthRequest); 1173 | 1174 | /** 1175 | * Makes an HTTP request. This method can be overridden by subclasses if 1176 | * developers want to do fancier things or use something other than curl to 1177 | * make the request. 1178 | * 1179 | * @param string host The Host to make the request to 1180 | * @param string path The URL to make the request to 1181 | * @param array params The parameters to use for the POST body 1182 | * @param callback 1183 | */ 1184 | BaseFacebook.prototype.makeRequest = function makeRequest(host, path, params, callback) { 1185 | requestUtil.requestFacebookApi(https, host, 443, path, params, this.fileUploadSupport, callback); 1186 | }; 1187 | 1188 | BaseFacebook.prototype.makeRequest = cb.wrap(BaseFacebook.prototype.makeRequest); 1189 | 1190 | /** 1191 | * Prints to the error log if you aren't in command line mode. 1192 | * 1193 | * @param string $msg Log message 1194 | */ 1195 | BaseFacebook.prototype.errorLog = function(msg) { 1196 | util.debug(msg); 1197 | }; 1198 | 1199 | /** 1200 | * Thrown when an API call returns an exception. 1201 | * 1202 | * @author Naitik Shah 1203 | */ 1204 | function FacebookApiError(result) { 1205 | this.result = result; 1206 | 1207 | this.code = this.result.hasOwnProperty('error_code') ? result.error_code : 0; 1208 | 1209 | if (result.hasOwnProperty('error_description')) { 1210 | // OAuth 2.0 Draft 10 style 1211 | var msg = result.error_description; 1212 | } else if (result.hasOwnProperty('error') && result.error && typeof (result.error) === 'object') { 1213 | // OAuth 2.0 Draft 00 style 1214 | var msg = result.error.message; 1215 | } else if (result.hasOwnProperty('error_msg')) { 1216 | // Rest server style 1217 | var msg = result.error_msg; 1218 | } else { 1219 | var msg = 'Unknown Error. Check getResult()'; 1220 | } 1221 | 1222 | Error.apply(this, []); 1223 | this.message = msg; 1224 | } 1225 | 1226 | util.inherits(FacebookApiError, Error); 1227 | 1228 | /** 1229 | * The result from the API server that represents the exception information. 1230 | */ 1231 | FacebookApiError.prototype.result = null; 1232 | 1233 | /** 1234 | * Return the associated result object returned by the API server. 1235 | * 1236 | * @return array The result from the API server 1237 | */ 1238 | FacebookApiError.prototype.getResult = function() { 1239 | return this.result; 1240 | }; 1241 | 1242 | /** 1243 | * Returns the associated type for the error. This will default to 1244 | * 'Error' when a type is not available. 1245 | * 1246 | * @return string 1247 | */ 1248 | FacebookApiError.prototype.getType = function() { 1249 | if (this.result.hasOwnProperty('error')) { 1250 | var error = this.result.error; 1251 | if (typeof error === 'string') { 1252 | // OAuth 2.0 Draft 10 style 1253 | return error; 1254 | } 1255 | else if (error && typeof error === 'object') { 1256 | // OAuth 2.0 Draft 00 style 1257 | if (error.hasOwnProperty('type')) { 1258 | return error.type; 1259 | } 1260 | } 1261 | } 1262 | 1263 | return 'Error'; 1264 | }; 1265 | 1266 | /** 1267 | * To make debugging easier. 1268 | * 1269 | * @return string The string representation of the error 1270 | */ 1271 | FacebookApiError.prototype.toString = function() { 1272 | var str = this.getType() + ': '; 1273 | if (this.code !== 0) { 1274 | str += this.code + ': '; 1275 | } 1276 | return str + this.message; 1277 | }; 1278 | 1279 | // for test 1280 | BaseFacebook.FacebookApiError = FacebookApiError; 1281 | 1282 | function isArray(ar) { 1283 | return Array.isArray(ar) || (typeof ar === 'object' && Object.prototype.toString.call(ar) === '[object Array]'); 1284 | } 1285 | 1286 | module.exports = BaseFacebook; 1287 | -------------------------------------------------------------------------------- /lib/cbutil.js: -------------------------------------------------------------------------------- 1 | 2 | var assert = require('assert'); 3 | 4 | /** 5 | * This function wrap a function that taken a callback: 6 | * - to callback when we catch error and 7 | * - to wrap the callback: 8 | * - to avoid throwing error in the callback and 9 | * - to avoid being called twice. 10 | * 11 | * @param fn function that taken a callback. 12 | * @param addError (optional) If this is true callback is not taken an error. And unshift callback arguments to pass an error. 13 | * @param callbackIndex (optional) argument index of callback 14 | */ 15 | exports.wrap = function wrap(fn, /* opt */addError, /* opt */callbackIndex) { 16 | if (fn.name === '__cbUtilWrapped__') { 17 | return fn; 18 | } 19 | return function __cbUtilWrapped__() { 20 | var index = (callbackIndex === undefined) ? arguments.length - 1 : callbackIndex; 21 | var callback = arguments[index]; 22 | if (typeof callback !== 'function') { 23 | throw new Error('Callback is not a function.'); 24 | } 25 | callback = wrapCallback(callback, addError); 26 | arguments[index] = callback; 27 | try { 28 | fn.apply(this, arguments); 29 | } 30 | catch (err) { 31 | callback(err, null); 32 | } 33 | }; 34 | }; 35 | 36 | exports.errorLog = function(msg) { 37 | console.error(msg); 38 | }; 39 | 40 | exports.returnToCallback = function(callback, handleError, fn) { 41 | return function() { 42 | try { 43 | if (handleError) { 44 | callback(null, fn.apply(this, arguments)); 45 | } 46 | else { 47 | var args = Array.prototype.slice.call(arguments); 48 | var e = args.shift(); 49 | if (e) { 50 | throw e; 51 | } 52 | callback(null, fn.apply(this, args)); 53 | } 54 | } 55 | catch (err) { 56 | callback(err, null); 57 | } 58 | }; 59 | }; 60 | 61 | function wrapCallback(callback, /* opt */addError) { 62 | assert.ok(callback.name !== '__cbUtilwrappedCallback__'); 63 | var called = false; 64 | return function __cbUtilWrappedCallback__() { 65 | if (called === true) { 66 | exports.errorLog(new Error('Cannot call callback twice.').stack); 67 | } 68 | else { 69 | called = true; 70 | try { 71 | if (addError === true) { 72 | // to array 73 | var args = Array.prototype.slice.call(arguments); 74 | args.unshift(null); 75 | callback.apply(this, args); 76 | } 77 | else { 78 | callback.apply(this, arguments); 79 | } 80 | } 81 | catch (err) { 82 | exports.errorLog('Callback cannot throw error: ' + err.stack); 83 | } 84 | } 85 | }; 86 | }; 87 | 88 | -------------------------------------------------------------------------------- /lib/facebook.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | var BaseFacebook = require(__dirname + '/basefacebook.js'); 4 | 5 | function Facebook(config) { 6 | this.hasSession = !!(config.request && config.request.session); 7 | BaseFacebook.apply(this, arguments); 8 | } 9 | 10 | util.inherits(Facebook, BaseFacebook); 11 | 12 | Facebook.prototype.setPersistentData = function(key, value) { 13 | if (this.hasSession) { 14 | this.request.session[key] = value; 15 | } 16 | }; 17 | 18 | Facebook.prototype.getPersistentData = function(key, defaultValue) { 19 | if (this.hasSession) { 20 | return this.request.session[key] || defaultValue; 21 | } 22 | return defaultValue; 23 | }; 24 | 25 | Facebook.prototype.clearPersistentData = function(key) { 26 | if (this.hasSession) { 27 | delete this.request.session[key]; 28 | } 29 | }; 30 | 31 | Facebook.prototype.clearAllPersistentData = function() { 32 | if (this.hasSession) { 33 | for (var name in this.sessionNameMap) { 34 | if (this.sessionNameMap.hasOwnProperty(name)) { 35 | this.clearPersistentData(this.sessionNameMap[name]); 36 | } 37 | } 38 | } 39 | }; 40 | 41 | Facebook.middleware = function(config) { 42 | return function(req, res, next) { 43 | config.request = req; 44 | config.response = res; 45 | req.facebook = new Facebook(config); 46 | next(); 47 | } 48 | }; 49 | 50 | Facebook.loginRequired = function(config) { 51 | return function(req, res, next) { 52 | if (!req.facebook) { 53 | Facebook.middleware(config)(req, res, afterNew); 54 | } 55 | else { 56 | afterNew(); 57 | } 58 | function afterNew() { 59 | req.facebook.getUser(function(err, user) { 60 | if (err) { 61 | next(err); 62 | next = null; 63 | } 64 | else { 65 | if (user === 0) { 66 | try { 67 | var loginUrl = req.facebook.getLoginUrl(config) 68 | } 69 | catch (err) { 70 | next(err); 71 | next = null; 72 | return; 73 | } 74 | res.redirect(loginUrl); 75 | next = null; 76 | } 77 | else { 78 | next(); 79 | next = null; 80 | } 81 | } 82 | }); 83 | } 84 | }; 85 | }; 86 | 87 | module.exports = Facebook; 88 | 89 | -------------------------------------------------------------------------------- /lib/multipart.js: -------------------------------------------------------------------------------- 1 | 2 | var assert = require('assert'); 3 | var path = require('path'); 4 | var fs = require('fs'); 5 | var cb = require('./cbutil'); 6 | 7 | function Multipart() { 8 | this.dash = new Buffer('--', 'ascii'); 9 | 10 | this.boundary = this.generateBoundary(); 11 | 12 | this.parts = []; 13 | } 14 | 15 | Multipart.prototype.dash = null; 16 | Multipart.prototype.boundary = null; 17 | Multipart.prototype.crlf = new Buffer('\r\n', 'ascii'); 18 | 19 | Multipart.prototype.generateBoundary = function generateBoundary() { 20 | return new Buffer( 21 | Math.floor(Math.random() * 0x80000000).toString(36) + 22 | Math.abs(Math.floor(Math.random() * 0x80000000) ^ +new Date()).toString(36), 23 | 'ascii' 24 | ); 25 | }; 26 | 27 | Multipart.prototype.addFile = function addFile(name, filePath, callback) { 28 | var self = this; 29 | fs.open(filePath, 'r', function(err, fd) { 30 | if (err) { 31 | callback(err); 32 | return; 33 | } 34 | try { 35 | fs.fstat(fd, cb.returnToCallback(callback, false, function(stat) { 36 | var fileName = path.basename(filePath); 37 | self.addStream(name, stat.size, fs.createReadStream(filePath, { fd: fd }), null, fileName); 38 | return null; 39 | })); 40 | } 41 | catch (e) { 42 | callback(e); 43 | } 44 | }); 45 | }; 46 | 47 | Multipart.prototype.addFile = cb.wrap(Multipart.prototype.addFile); 48 | 49 | Multipart.prototype.addBuffer = function addBuffer(name, buffer, mime, fileName) { 50 | this.parts.push({ 51 | type: 'buffer', 52 | name: new Buffer(name, 'utf8'), 53 | fileName: typeof fileName === 'string' ? new Buffer(fileName, 'utf8') : null, 54 | buffer: buffer, 55 | size: buffer.length, 56 | mime: new Buffer(mime || 'application/octet-stream', 'ascii') 57 | }); 58 | }; 59 | 60 | Multipart.prototype.addStream = function addStream(name, size, stream, mime, fileName) { 61 | stream.pause(); 62 | this.parts.push({ 63 | type: 'stream', 64 | name: new Buffer(name, 'utf8'), 65 | fileName: typeof fileName === 'string' ? new Buffer(fileName, 'utf8') : null, 66 | stream: stream, 67 | size: size, 68 | mime: new Buffer(mime || 'application/octet-stream', 'ascii') 69 | }); 70 | }; 71 | 72 | Multipart.prototype.addText = function addText(name, text) { 73 | var buffer = new Buffer(text, 'ascii'); 74 | this.addBuffer(name, buffer, 'text/plain; charset=UTF-8'); 75 | }; 76 | 77 | Multipart.prototype.contentTypeValuePrefix = new Buffer('multipart/form-data; boundary=', 'ascii'); 78 | 79 | Multipart.prototype.getContentType = function getContentType() { 80 | var buffer = new Buffer(this.contentTypeValuePrefix.length + this.boundary.length); 81 | this.contentTypeValuePrefix.copy(buffer); 82 | this.boundary.copy(buffer, this.contentTypeValuePrefix.length); 83 | return buffer; 84 | }; 85 | 86 | Multipart.prototype.contentDispositionPrefix = new Buffer('Content-Disposition: form-data; name="', 'ascii'); 87 | Multipart.prototype.contentDispositionSuffix = new Buffer('"', 'ascii'); 88 | Multipart.prototype.contentDispositionFilenamePrefix = new Buffer('; filename="', 'ascii'); 89 | Multipart.prototype.contentDispositionFilenameSuffix = new Buffer('"', 'ascii'); 90 | Multipart.prototype.partContentTypePrefix = new Buffer('Content-Type: ', 'ascii'); 91 | 92 | Multipart.prototype.getContentLength = function getContentLength() { 93 | 94 | var self = this; 95 | var length = this.parts.reduce(function(sum, part) { 96 | var partLength = self.dash.length + 97 | self.boundary.length + 98 | self.crlf.length; 99 | 100 | partLength += self.contentDispositionPrefix.length + 101 | part.name.length + 102 | self.contentDispositionSuffix.length; 103 | if (part.fileName !== null) { 104 | partLength += self.contentDispositionFilenamePrefix.length + 105 | part.fileName.length + 106 | self.contentDispositionFilenameSuffix.length; 107 | } 108 | partLength += self.crlf.length; 109 | 110 | partLength += self.partContentTypePrefix.length + 111 | part.mime.length + 112 | self.crlf.length + 113 | self.crlf.length; 114 | 115 | partLength += part.size + self.crlf.length; 116 | 117 | return sum + partLength; 118 | }, 0); 119 | 120 | length += self.dash.length + 121 | self.boundary.length + 122 | self.dash.length + 123 | self.crlf.length; 124 | 125 | return length; 126 | }; 127 | 128 | Multipart.prototype.writeToStream = function writeToStream(stream, callback) { 129 | var self = this; 130 | var parts = this.parts; 131 | 132 | var entities = []; 133 | 134 | for (var i = 0; i < parts.length; i++) { 135 | var part = parts[i]; 136 | 137 | entities.push(self.dash); 138 | entities.push(self.boundary); 139 | entities.push(self.crlf); 140 | 141 | entities.push(self.contentDispositionPrefix); 142 | entities.push(part.name); 143 | entities.push(self.contentDispositionSuffix); 144 | if (part.fileName !== null) { 145 | entities.push(self.contentDispositionFilenamePrefix); 146 | entities.push(part.fileName); 147 | entities.push(self.contentDispositionFilenameSuffix); 148 | } 149 | entities.push(self.crlf); 150 | 151 | entities.push(self.partContentTypePrefix); 152 | entities.push(part.mime); 153 | entities.push(self.crlf); 154 | entities.push(self.crlf); 155 | 156 | if (part.type === 'buffer') { 157 | entities.push(part.buffer); 158 | entities.push(self.crlf); 159 | } 160 | else { 161 | entities.push(part.stream); 162 | entities.push(self.crlf); 163 | } 164 | } 165 | 166 | entities.push(self.dash); 167 | entities.push(self.boundary); 168 | entities.push(self.dash); 169 | entities.push(self.crlf); 170 | 171 | function write(stream, entities) { 172 | var entity = entities[0]; 173 | if (entity === undefined) { 174 | callback(null); 175 | return; 176 | } 177 | try { 178 | if (entity instanceof Buffer) { 179 | var buffer = entity; 180 | if (stream.write(buffer)) { 181 | write(stream, entities.slice(1)); 182 | } 183 | else { 184 | stream.once('drain', function() { 185 | try { 186 | write(stream, entities.slice(1)); 187 | } 188 | catch (err) { 189 | callback(err); 190 | } 191 | }); 192 | } 193 | } 194 | else { 195 | var readableStream = entity; 196 | var readableStreamOnError = function(err) { 197 | try { readableStream.removeListener('error', readableStreamOnError) } catch (e) { } 198 | try { readableStream.removeListener('end', readableStreamOnEnd) } catch (e) { } 199 | try { readableStream.destroy() } catch (e) { } 200 | callback(err); 201 | }; 202 | var readableStreamOnEnd = function() { 203 | try { 204 | readableStream.removeListener('error', readableStreamOnError); 205 | readableStream.removeListener('end', readableStreamOnEnd); 206 | write(stream, entities.slice(1)); 207 | } 208 | catch (err) { 209 | callback(err); 210 | } 211 | }; 212 | readableStream.on('error', readableStreamOnError); 213 | readableStream.on('end', readableStreamOnEnd); 214 | readableStream.pipe(stream, { end: false }); 215 | readableStream.resume(); 216 | } 217 | } 218 | catch (err) { 219 | callback(err); 220 | } 221 | } 222 | 223 | write(stream, entities); 224 | }; 225 | 226 | Multipart.prototype.writeToStream = cb.wrap(Multipart.prototype.writeToStream); 227 | 228 | module.exports = Multipart; 229 | 230 | -------------------------------------------------------------------------------- /lib/requestutil.js: -------------------------------------------------------------------------------- 1 | 2 | var assert = require('assert'); 3 | var querystring = require('querystring'); 4 | var Multipart = require('./multipart'); 5 | 6 | exports.requestFacebookApi = function(http, host, port, path, params, withMultipart, callback) { 7 | var req = new FacebookApiRequest(http, host, port, path, params); 8 | req.start(withMultipart, callback); 9 | }; 10 | 11 | // export for debug 12 | exports.FacebookApiRequest = FacebookApiRequest; 13 | 14 | function bindSelf(self, fn) { 15 | return function selfBoundFunction() { 16 | return fn.apply(self, arguments); 17 | }; 18 | } 19 | 20 | function FacebookApiRequest(http, host, port, path, params) { 21 | assert.equal(this.http, null); 22 | assert.equal(this.host, null); 23 | assert.equal(this.port, null); 24 | assert.equal(this.path, null); 25 | assert.equal(this.params, null); 26 | 27 | // TODO request timeout setting 28 | // TODO user agent setting 29 | 30 | this.http = http; 31 | this.host = host; 32 | this.port = port; 33 | this.path = path; 34 | this.params = params; 35 | 36 | this.selfBoundResponseErrorHandler = bindSelf(this, this.handleResponseError); 37 | this.selfBoundResponseHandler = bindSelf(this, this.handleResponse); 38 | this.selfBoundDataHandler = bindSelf(this, this.handleData); 39 | this.selfBoundDataErrorHandler = bindSelf(this, this.handleDataError); 40 | this.selfBoundEndHandler = bindSelf(this, this.handleEnd); 41 | } 42 | 43 | FacebookApiRequest.prototype.http = null; 44 | FacebookApiRequest.prototype.host = null; 45 | FacebookApiRequest.prototype.port = null; 46 | FacebookApiRequest.prototype.path = null; 47 | FacebookApiRequest.prototype.params = null; 48 | FacebookApiRequest.prototype.callback = null; 49 | FacebookApiRequest.prototype.selfBoundResponseErrorHandler = null; 50 | FacebookApiRequest.prototype.selfBoundResponseHandler = null; 51 | FacebookApiRequest.prototype.selfBoundDataHandler = null; 52 | FacebookApiRequest.prototype.selfBoundDataErrorHandler = null; 53 | FacebookApiRequest.prototype.selfBoundEndHandler = null; 54 | 55 | FacebookApiRequest.prototype.start = function(withMultipart, callback) { 56 | assert.equal(this.req, null); 57 | assert.equal(this.callback, null); 58 | 59 | this.callback = callback; 60 | 61 | if (withMultipart) { 62 | var multipart = new Multipart(); 63 | var keys = Object.keys(this.params); 64 | var self = this; 65 | (function loop() { 66 | try { 67 | var key = keys.shift(); 68 | if (key === undefined) { 69 | afterParams(); 70 | return; 71 | } 72 | if (self.params[key].charAt(0) === '@') { 73 | multipart.addFile(key, self.params[key].substr(1), function(err) { 74 | if (err) { 75 | callback(err, null); 76 | } 77 | else { 78 | loop(); 79 | } 80 | }); 81 | } 82 | else { 83 | multipart.addText(key, self.params[key]); 84 | loop(); 85 | } 86 | } 87 | catch (err) { 88 | callback(err, null); 89 | } 90 | })(); 91 | function afterParams() { 92 | try { 93 | var options = { 94 | host: self.host, 95 | path: self.path, 96 | port: self.port, 97 | method: 'POST', 98 | headers: { 99 | 'Content-Type': multipart.getContentType(), 100 | 'Content-Length': multipart.getContentLength() 101 | } 102 | }; 103 | self.req = self.http.request(options); 104 | self.req.on('error', self.selfBoundResponseErrorHandler); 105 | self.req.on('response', self.selfBoundResponseHandler); 106 | multipart.writeToStream(self.req, function(err) { 107 | if (err) { 108 | onerror(err); 109 | } 110 | else { 111 | self.req.end(); 112 | } 113 | }); 114 | } 115 | catch (err) { 116 | onerror(err); 117 | } 118 | function onerror(err) { 119 | if (self.req) { 120 | self.callQuietly(self.detachResponseAndErrorHandlers); 121 | self.callQuietly(self.abortRequest); 122 | } 123 | callback(err, null); 124 | } 125 | } 126 | } 127 | else { 128 | // Querystring is encoding multibyte as utf-8. 129 | var postData = querystring.stringify(this.params); 130 | 131 | var options = { 132 | host: this.host, 133 | path: this.path, 134 | port: this.port, 135 | method: 'POST', 136 | headers: { 137 | 'Content-Type': 'application/x-www-form-urlencoded', 138 | 'Content-Length': postData.length 139 | } 140 | }; 141 | 142 | this.req = this.http.request(options); 143 | this.req.on('error', this.selfBoundResponseErrorHandler); 144 | this.req.on('response', this.selfBoundResponseHandler); 145 | this.req.end(postData); 146 | } 147 | }; 148 | 149 | FacebookApiRequest.prototype.req = null; 150 | 151 | FacebookApiRequest.prototype.handleResponse = function(res) { 152 | assert.notEqual(this.callback, null); 153 | 154 | try { 155 | this.detachResponseAndErrorHandlers(); 156 | this.afterResponse(res); 157 | } 158 | catch (err) { 159 | this.callback(err, null); 160 | } 161 | }; 162 | 163 | FacebookApiRequest.prototype.handleResponseError = function (err) { 164 | assert.notEqual(this.callback, null); 165 | 166 | this.callQuietly(this.detachResponseAndErrorHandlers); 167 | this.callQuietly(this.abortRequest); 168 | this.callback(err, null); 169 | }; 170 | 171 | FacebookApiRequest.prototype.detachResponseAndErrorHandlers = function() { 172 | assert.notEqual(this.req, null); 173 | 174 | this.req.removeListener('error', this.selfBoundResponseErrorHandler); 175 | this.req.removeListener('response', this.selfBoundResponseHandler); 176 | }; 177 | 178 | FacebookApiRequest.prototype.afterResponse = function(res) { 179 | assert.equal(this.res, null); 180 | assert.equal(this.responseBody, null); 181 | 182 | this.res = res; 183 | this.res.setEncoding('utf8'); 184 | this.responseBody = []; 185 | this.res.on('data', this.selfBoundDataHandler); 186 | this.res.on('error', this.selfBoundDataErrorHandler); 187 | this.res.on('end', this.selfBoundEndHandler); 188 | }; 189 | 190 | FacebookApiRequest.prototype.res = null; 191 | FacebookApiRequest.prototype.responseBody = null; 192 | 193 | FacebookApiRequest.prototype.handleData = function(data) { 194 | assert.notEqual(this.responseBody, null); 195 | 196 | this.responseBody.push(data); 197 | }; 198 | 199 | FacebookApiRequest.prototype.handleDataError = function (err) { 200 | this.callQuietly(this.detachDataAndEndAndErrorHandlers); 201 | this.callQuietly(this.abortRequest); 202 | this.callback(err, null); 203 | }; 204 | 205 | FacebookApiRequest.prototype.handleEnd = function() { 206 | assert.notEqual(this.responseBody, null); 207 | assert.notEqual(this.callback, null); 208 | 209 | try { 210 | this.detachDataAndEndAndErrorHandlers(); 211 | this.callback(null, this.responseBody.join('')); 212 | } 213 | catch (err) { 214 | this.callback(err, null); 215 | } 216 | }; 217 | 218 | FacebookApiRequest.prototype.detachDataAndEndAndErrorHandlers = function() { 219 | this.res.removeListener('data', this.selfBoundDataHandler); 220 | this.res.removeListener('error', this.selfBoundDataErrorHandler); 221 | this.res.removeListener('end', this.selfBoundEndHandler); 222 | }; 223 | 224 | FacebookApiRequest.prototype.abortRequest = function() { 225 | assert.notEqual(this.req, null); 226 | this.req.abort(); 227 | }; 228 | 229 | FacebookApiRequest.prototype.callQuietly = function() { 230 | try { 231 | var args = [].slice.call(arguments); 232 | var fn = args.shift(); 233 | return fn.apply(this, args); 234 | } 235 | catch (err) { 236 | // ignore error 237 | } 238 | }; 239 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "facebook-node-sdk", 3 | "version": "0.2.0", 4 | "description": "Node.js SDK for the Facebook API", 5 | "tags": [ 6 | "facebook" 7 | ], 8 | "author": { 9 | "name": "Hitoshi Amano", 10 | "email": "seijro@gmail.com" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/amachang/facebook-node-sdk.git" 15 | }, 16 | "scripts": { 17 | "test": "node_modules/.bin/expresso" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/amachang/facebook-node-sdk/issues" 21 | }, 22 | "license": [ 23 | { 24 | "type": "MIT", 25 | "url": "https://raw.github.com/amachang/facebook-node-sdk/master/LICENSE" 26 | } 27 | ], 28 | "devDependencies": { 29 | "expresso": ">=0.9.2", 30 | "express": ">=2.5.1" 31 | }, 32 | "main": "./lib/facebook", 33 | "readme": "# Facebook Node SDK\n\nFacebook API Implementation in Node.\n\n[![Build Status](https://secure.travis-ci.org/amachang/facebook-node-sdk.png)](http://travis-ci.org/amachang/facebook-node-sdk)\n\n## Features\n\n* Supports all Facebook Graph API, FQL, and REST API.\n* Compatible with the official Facebook PHP SDK.\n\n## Install\n\nTo install the most recent release from npm, run:\n\n npm install facebook-node-sdk\n\n## Synopsis\n\n var Facebook = require('facebook-node-sdk');\n \n var facebook = new Facebook({ appID: 'YOUR_APP_ID', secret: 'YOUR_APP_SECRET' });\n \n facebook.api('/amachang', function(err, data) {\n console.log(data); // => { id: ... }\n });\n\n### With express framework (as connect middleware)\n\n var express = require('express');\n var Facebook = require('facebook-node-sdk');\n \n var app = express.createServer();\n \n app.configure(function () {\n app.use(express.bodyParser());\n app.use(express.cookieParser());\n app.use(express.session({ secret: 'foo bar' }));\n app.use(Facebook.middleware({ appId: 'YOUR_APP_ID', secret: 'YOUR_APP_SECRET' }));\n });\n \n app.get('/', Facebook.loginRequired(), function (req, res) {\n req.facebook.api('/me', function(err, user) {\n res.writeHead(200, {'Content-Type': 'text/plain'});\n res.end('Hello, ' + user.name + '!');\n });\n });\n\n", 34 | "readmeFilename": "README.md", 35 | "_id": "facebook-node-sdk@0.2.0", 36 | "dist": { 37 | "shasum": "538604a2598fbcbf7f973b19e6b3dea04d2a8169" 38 | }, 39 | "_resolved": "git://github.com/Costent/facebook-node-sdk.git#640d248881555f3718858eb551851867ae634484", 40 | "_from": "facebook-node-sdk@git://github.com/Costent/facebook-node-sdk.git" 41 | } 42 | -------------------------------------------------------------------------------- /test/basefacebook.test.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var util = require('util'); 3 | var url = require('url'); 4 | var testUtil = require('./lib/testutil.js'); 5 | 6 | var BaseFacebook = require(path.join(testUtil.libdir, 'basefacebook.js')); 7 | 8 | var config = testUtil.fbDefaultConfig; 9 | 10 | module.exports = { 11 | 12 | constructor: function(beforeExit, assert) { 13 | var done = false; 14 | beforeExit(function() { assert.ok(done) }); 15 | var done = false; 16 | beforeExit(function() { assert.ok(done) }); 17 | var facebook = new TransientFacebook({ 18 | appId: config.appId, 19 | secret: config.secret 20 | }); 21 | assert.equal(facebook.getAppId(), config.appId, 'Expect the App ID to be set.'); 22 | assert.equal(facebook.getAppSecret(), config.secret, 'Expect the app secret to be set.'); 23 | // for compatibility 24 | assert.equal(facebook.getApiSecret(), config.secret, 'Expect the app secret to be set.'); 25 | assert.equal(facebook.getApplicationAccessToken(), config.appId + '|' + config.secret); 26 | done = true; 27 | }, 28 | 29 | constructorWithFileUpload: function(beforeExit, assert) { 30 | var done = false; 31 | beforeExit(function() { assert.ok(done) }); 32 | var done = false; 33 | beforeExit(function() { assert.ok(done) }); 34 | var facebook = new TransientFacebook({ 35 | appId: config.appId, 36 | secret: config.secret, 37 | fileUpload: true 38 | }); 39 | assert.equal(facebook.getAppId(), config.appId, 'Expect the App ID to be set.'); 40 | assert.equal(facebook.getAppSecret(), config.secret, 'Expect the app secret to be set.'); 41 | // for compatibility 42 | assert.equal(facebook.getApiSecret(), config.secret, 'Expect the app secret to be set.'); 43 | assert.ok(facebook.getFileUploadSupport(), 'Expect file upload support to be on.'); 44 | // alias (depricated) for getFileUploadSupport -- test until removed 45 | assert.ok(facebook.useFileUploadSupport(), 'Expect file upload support to be on.'); 46 | done = true; 47 | }, 48 | 49 | setAppId: function(beforeExit, assert) { 50 | var done = false; 51 | beforeExit(function() { assert.ok(done) }); 52 | var facebook = new TransientFacebook({ 53 | appId: config.appId, 54 | secret: config.secret 55 | }); 56 | facebook.setAppId('dummy'); 57 | assert.equal(facebook.getAppId(), 'dummy', 'Expect the App ID to be dummy.'); 58 | done = true; 59 | }, 60 | 61 | // for compatibility 62 | setApiSecret: function(beforeExit, assert) { 63 | var done = false; 64 | beforeExit(function() { assert.ok(done) }); 65 | var facebook = new TransientFacebook({ 66 | appId: config.appId, 67 | secret: config.secret 68 | }); 69 | facebook.setApiSecret('dummy'); 70 | assert.equal(facebook.getApiSecret(), 'dummy', 'Expect the app secret to be dummy.'); 71 | done = true; 72 | }, 73 | 74 | setAppSecret: function(beforeExit, assert) { 75 | var done = false; 76 | beforeExit(function() { assert.ok(done) }); 77 | var facebook = new TransientFacebook({ 78 | appId: config.appId, 79 | secret: config.secret 80 | }); 81 | facebook.setAppSecret('dummy'); 82 | assert.equal(facebook.getAppSecret(), 'dummy', 'Expect the app secret to be dummy.'); 83 | done = true; 84 | }, 85 | 86 | setAccessToken: function(beforeExit, assert) { 87 | var done = false; 88 | beforeExit(function() { assert.ok(done) }); 89 | var facebook = new TransientFacebook({ 90 | appId: config.appId, 91 | secret: config.secret 92 | }); 93 | 94 | facebook.setAccessToken('saltydog'); 95 | facebook.getAccessToken(function(err, accessToken) { 96 | assert.equal(err, null); 97 | assert.equal(accessToken, 'saltydog', 98 | 'Expect installed access token to remain \'saltydog\''); 99 | done = true; 100 | }); 101 | }, 102 | 103 | setFileUploadSupport: function(beforeExit, assert) { 104 | var done = false; 105 | beforeExit(function() { assert.ok(done) }); 106 | var facebook = new TransientFacebook({ 107 | appId: config.appId, 108 | secret: config.secret 109 | }); 110 | assert.equal(facebook.getFileUploadSupport(), false, 'Expect file upload support to be off.'); 111 | // alias for getFileUploadSupport (depricated), testing until removed 112 | assert.equal(facebook.useFileUploadSupport(), false, 'Expect file upload support to be off.'); 113 | facebook.setFileUploadSupport(true); 114 | assert.ok(facebook.getFileUploadSupport(), 'Expect file upload support to be on.'); 115 | // alias for getFileUploadSupport (depricated), testing until removed 116 | assert.ok(facebook.useFileUploadSupport(), 'Expect file upload support to be on.'); 117 | done = true; 118 | }, 119 | 120 | getCurrentUrl: function(beforeExit, assert) { 121 | var done = false; 122 | beforeExit(function() { assert.ok(done) }); 123 | var facebook = new TransientFacebook({ 124 | appId: config.appId, 125 | secret: config.secret, 126 | request: { 127 | connection: { 128 | }, 129 | headers: { 130 | host: 'www.test.com' 131 | }, 132 | url: '/unit-tests.php?one=one&two=two&three=three' 133 | } 134 | }); 135 | 136 | var currentUrl = facebook.getCurrentUrl(); 137 | assert.equal('http://www.test.com/unit-tests.php?one=one&two=two&three=three', 138 | currentUrl, 'getCurrentUrl function is changing the current URL'); 139 | 140 | facebook = new TransientFacebook({ 141 | appId: config.appId, 142 | secret: config.secret, 143 | request: { 144 | connection: { 145 | }, 146 | headers: { 147 | host: 'www.test.com' 148 | }, 149 | url: '/unit-tests.php?one=&two=&three=' 150 | } 151 | }); 152 | 153 | currentUrl = facebook.getCurrentUrl(); 154 | assert.equal('http://www.test.com/unit-tests.php?one=&two=&three=', 155 | currentUrl, 'getCurrentUrl function is changing the current URL'); 156 | 157 | facebook = new TransientFacebook({ 158 | appId: config.appId, 159 | secret: config.secret, 160 | request: { 161 | connection: { 162 | }, 163 | headers: { 164 | host: 'www.test.com' 165 | }, 166 | url: '/unit-tests.php?one&two&three' 167 | } 168 | }); 169 | 170 | currentUrl = facebook.getCurrentUrl(); 171 | assert.equal('http://www.test.com/unit-tests.php?one&two&three', 172 | currentUrl, 'getCurrentUrl function is changing the current URL'); 173 | 174 | facebook = new TransientFacebook({ 175 | appId: config.appId, 176 | secret: config.secret, 177 | request: { 178 | connection: { 179 | }, 180 | headers: { 181 | host: 'www.test.com' 182 | }, 183 | url: '/unit-tests.php?one&two&three&state=hoge' 184 | } 185 | }); 186 | 187 | currentUrl = facebook.getCurrentUrl(); 188 | assert.equal('http://www.test.com/unit-tests.php?one&two&three', 189 | currentUrl, 'getCurrentUrl function is changing the current URL'); 190 | 191 | facebook = new TransientFacebook({ 192 | appId: config.appId, 193 | secret: config.secret, 194 | request: { 195 | connection: { 196 | }, 197 | headers: { 198 | host: 'www.test.com' 199 | }, 200 | url: '/unit-tests.php?state=hoge' 201 | } 202 | }); 203 | 204 | currentUrl = facebook.getCurrentUrl(); 205 | assert.equal('http://www.test.com/unit-tests.php', currentUrl); 206 | 207 | facebook = new TransientFacebook({ 208 | appId: config.appId, 209 | secret: config.secret, 210 | request: { 211 | connection: { 212 | }, 213 | headers: { 214 | host: 'www.test.com' 215 | }, 216 | url: '/unit-tests.php?state=hoge&base_domain=test.com' 217 | } 218 | }); 219 | 220 | currentUrl = facebook.getCurrentUrl(); 221 | assert.equal('http://www.test.com/unit-tests.php', currentUrl); 222 | 223 | facebook = new TransientFacebook({ 224 | appId: config.appId, 225 | secret: config.secret, 226 | currentUrl: 'http://example.com/', 227 | request: { 228 | connection: { 229 | }, 230 | headers: { 231 | host: 'www.test.com' 232 | }, 233 | url: '/unit-tests.php?state=hoge' 234 | } 235 | }); 236 | 237 | assert.equal(facebook.getCurrentUrl(), 'http://example.com/'); 238 | 239 | done = true; 240 | }, 241 | 242 | getLoginUrl: function(beforeExit, assert) { 243 | var done = false; 244 | beforeExit(function() { assert.ok(done) }); 245 | var facebook = new TransientFacebook({ 246 | appId: config.appId, 247 | secret: config.secret, 248 | request: { 249 | connection: { 250 | }, 251 | headers: { 252 | host: 'www.test.com' 253 | }, 254 | url: '/unit-tests.php' 255 | } 256 | }); 257 | 258 | var loginUrl = url.parse(facebook.getLoginUrl(), true); 259 | assert.equal(loginUrl.protocol, 'https:'); 260 | assert.equal(loginUrl.host, 'www.facebook.com'); 261 | assert.equal(loginUrl.pathname, '/dialog/oauth'); 262 | assert.equal(loginUrl.query.client_id, config.appId); 263 | assert.equal(loginUrl.query.redirect_uri, 'http://www.test.com/unit-tests.php'); 264 | assert.equal(loginUrl.query.state.length, 32); 265 | done = true; 266 | }, 267 | 268 | getLoginURLWithExtraParams: function(beforeExit, assert) { 269 | var done = false; 270 | beforeExit(function() { assert.ok(done) }); 271 | var facebook = new TransientFacebook({ 272 | appId: config.appId, 273 | secret: config.secret, 274 | request: { 275 | connection: { 276 | }, 277 | headers: { 278 | host: 'www.test.com' 279 | }, 280 | url: '/unit-tests.php' 281 | } 282 | }); 283 | 284 | var extraParams = { 285 | scope: 'email, sms', 286 | nonsense: 'nonsense' 287 | }; 288 | var loginUrl = url.parse(facebook.getLoginUrl(extraParams), true); 289 | assert.equal(loginUrl.protocol, 'https:'); 290 | assert.equal(loginUrl.host, 'www.facebook.com'); 291 | assert.equal(loginUrl.pathname, '/dialog/oauth'); 292 | assert.equal(loginUrl.query.client_id, config.appId); 293 | assert.equal(loginUrl.query.redirect_uri, 'http://www.test.com/unit-tests.php'); 294 | assert.equal(loginUrl.query.scope, extraParams.scope); 295 | assert.equal(loginUrl.query.nonsense, extraParams.nonsense); 296 | assert.equal(loginUrl.query.state.length, 32); 297 | done = true; 298 | }, 299 | 300 | getLoginURLWithScopeParamsAsArray: function(beforeExit, assert) { 301 | var done = false; 302 | beforeExit(function() { assert.ok(done) }); 303 | var facebook = new TransientFacebook({ 304 | appId: config.appId, 305 | secret: config.secret, 306 | request: { 307 | connection: { 308 | }, 309 | headers: { 310 | host: 'www.test.com' 311 | }, 312 | url: '/unit-tests.php' 313 | } 314 | }); 315 | 316 | var scopeParamsAsArray = ['email','sms','read_stream']; 317 | var extraParams = { 318 | scope: scopeParamsAsArray, 319 | nonsense: 'nonsense' 320 | }; 321 | var loginUrl = url.parse(facebook.getLoginUrl(extraParams), true); 322 | assert.equal(loginUrl.protocol, 'https:'); 323 | assert.equal(loginUrl.host, 'www.facebook.com'); 324 | assert.equal(loginUrl.pathname, '/dialog/oauth'); 325 | assert.equal(loginUrl.query.client_id, config.appId); 326 | assert.equal(loginUrl.query.redirect_uri, 'http://www.test.com/unit-tests.php'); 327 | assert.equal(loginUrl.query.scope, scopeParamsAsArray.join(',')); 328 | assert.equal(loginUrl.query.nonsense, extraParams.nonsense); 329 | assert.equal(loginUrl.query.state.length, 32); 330 | done = true; 331 | }, 332 | 333 | getCodeWithValidCSRFState: function(beforeExit, assert) { 334 | var done = false; 335 | beforeExit(function() { assert.ok(done) }); 336 | var facebook = new TransientFacebook({ 337 | appId: config.appId, 338 | secret: config.secret, 339 | request: { 340 | query: {} 341 | } 342 | }); 343 | 344 | facebook.establishCSRFTokenState(); 345 | 346 | var code = facebook.request.query.code = 'dummy'; 347 | facebook.request.query.state = facebook.getPersistentData('state'); 348 | assert.equal(code, facebook.getCode(), 'Expect code to be pulled from $_REQUEST[\'code\']'); 349 | done = true; 350 | }, 351 | 352 | getCodeWithInvalidCSRFState: function(beforeExit, assert) { 353 | var done = false; 354 | beforeExit(function() { assert.ok(done) }); 355 | var facebook = new TransientFacebook({ 356 | appId: config.appId, 357 | secret: config.secret, 358 | request: { 359 | query: {} 360 | } 361 | }); 362 | 363 | facebook.establishCSRFTokenState(); 364 | 365 | var code = facebook.request.query.code = 'dummy'; 366 | facebook.request.query.state = facebook.getPersistentData('state') + 'forgery!!!'; 367 | facebook.errorLog = function() {}; 368 | assert.equal(facebook.getCode(), false, 'Expect getCode to fail, CSRF state should not match.'); 369 | done = true; 370 | }, 371 | 372 | getCodeWithMissingCSRFState: function(beforeExit, assert) { 373 | var done = false; 374 | beforeExit(function() { assert.ok(done) }); 375 | var facebook = new TransientFacebook({ 376 | appId: config.appId, 377 | secret: config.secret, 378 | request: { 379 | query: {} 380 | } 381 | }); 382 | 383 | var code = facebook.request.query.code = 'dummy'; 384 | // intentionally don't set CSRF token at all 385 | facebook.errorLog = function() {}; 386 | assert.equal(facebook.getCode(), false, 'Expect getCode to fail, CSRF state not sent back.'); 387 | done = true; 388 | }, 389 | 390 | getUserFromSignedRequest: function(beforeExit, assert) { 391 | var done = false; 392 | beforeExit(function() { assert.ok(done) }); 393 | var facebook = new TransientFacebook({ 394 | appId: '117743971608120', 395 | secret: '943716006e74d9b9283d4d5d8ab93204', 396 | request: { 397 | body: { 398 | signed_request: '1sxR88U4SW9m6QnSxwCEw_CObqsllXhnpP5j2pxD97c.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsImV4cGlyZXMiOjEyODEwNTI4MDAsIm9hdXRoX3Rva2VuIjoiMTE3NzQzOTcxNjA4MTIwfDIuVlNUUWpub3hYVVNYd1RzcDB1U2g5d19fLjg2NDAwLjEyODEwNTI4MDAtMTY3Nzg0NjM4NXx4NURORHBtcy1nMUM0dUJHQVYzSVdRX2pYV0kuIiwidXNlcl9pZCI6IjE2Nzc4NDYzODUifQ' 399 | } 400 | } 401 | }); 402 | 403 | facebook.getUser(function(err, userId) { 404 | assert.equal(err, null); 405 | assert.equal('1677846385', userId, 'Failed to get user ID from a valid signed request.'); 406 | done = true; 407 | }); 408 | }, 409 | 410 | getSignedRequestFromCookie: function(beforeExit, assert) { 411 | var done = false; 412 | beforeExit(function() { assert.ok(done) }); 413 | var facebook = new TransientFacebook({ 414 | appId: '117743971608120', 415 | secret: '943716006e74d9b9283d4d5d8ab93204', 416 | request: { 417 | cookies: { 418 | } 419 | } 420 | }); 421 | 422 | facebook.request.cookies[facebook.getSignedRequestCookieName()] = '1sxR88U4SW9m6QnSxwCEw_CObqsllXhnpP5j2pxD97c.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsImV4cGlyZXMiOjEyODEwNTI4MDAsIm9hdXRoX3Rva2VuIjoiMTE3NzQzOTcxNjA4MTIwfDIuVlNUUWpub3hYVVNYd1RzcDB1U2g5d19fLjg2NDAwLjEyODEwNTI4MDAtMTY3Nzg0NjM4NXx4NURORHBtcy1nMUM0dUJHQVYzSVdRX2pYV0kuIiwidXNlcl9pZCI6IjE2Nzc4NDYzODUifQ'; 423 | assert.notEqual(facebook.getSignedRequest(), null); 424 | facebook.getUser(function(err, userId) { 425 | assert.equal(err, null); 426 | assert.equal('1677846385', userId, 'Failed to get user ID from a valid signed request.'); 427 | done = true; 428 | }); 429 | }, 430 | 431 | getSignedRequestWithIncorrectSignature: function(beforeExit, assert) { 432 | var done = false; 433 | beforeExit(function() { assert.ok(done) }); 434 | var facebook = new TransientFacebook({ 435 | appId: '117743971608120', 436 | secret: '943716006e74d9b9283d4d5d8ab93204', 437 | request: { 438 | cookies: { 439 | } 440 | } 441 | }); 442 | 443 | facebook.request.cookies[facebook.getSignedRequestCookieName()] = '1sxR32U4SW9m6QnSxwCEw_CObqsllXhnpP5j2pxD97c.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsImV4cGlyZXMiOjEyODEwNTI4MDAsIm9hdXRoX3Rva2VuIjoiMTE3NzQzOTcxNjA4MTIwfDIuVlNUUWpub3hYVVNYd1RzcDB1U2g5d19fLjg2NDAwLjEyODEwNTI4MDAtMTY3Nzg0NjM4NXx4NURORHBtcy1nMUM0dUJHQVYzSVdRX2pYV0kuIiwidXNlcl9pZCI6IjE2Nzc4NDYzODUifQ'; 444 | facebook.errorLog = function() { }; 445 | assert.equal(facebook.getSignedRequest(), null); 446 | facebook.getUser(function(err, userId) { 447 | assert.equal(err, null); 448 | assert.equal(0, userId, 'Failed to get user ID from a valid signed request.'); 449 | done = true; 450 | }); 451 | }, 452 | 453 | nonUserAccessToken: function(beforeExit, assert) { 454 | var done = false; 455 | beforeExit(function() { assert.ok(done) }); 456 | var facebook = new TransientFacebook({ 457 | appId: config.appId, 458 | secret: config.secret 459 | }); 460 | 461 | // no cookies, and no request params, so no user or code, 462 | // so no user access token (even with cookie support) 463 | facebook.getAccessToken(function(err, accessToken) { 464 | assert.equal(err, null); 465 | assert.equal(facebook.getApplicationAccessToken(), accessToken, 466 | 'Access token should be that for logged out users.'); 467 | done = true; 468 | }); 469 | }, 470 | 471 | getAccessTokenFromCode: function(beforeExit, assert) { 472 | var done = false; 473 | beforeExit(function() { assert.ok(done) }); 474 | var facebook = new TransientFacebook({ 475 | appId: config.appId, 476 | secret: config.secret, 477 | request: { 478 | connection: {}, 479 | headers: {} 480 | } 481 | }); 482 | facebook.getAccessTokenFromCode('dummy', '', function(err, response) { 483 | assert.equal(err, null); 484 | assert.equal(response, false); 485 | facebook.getAccessTokenFromCode(null, '', function() { 486 | assert.equal(err, null); 487 | assert.equal(response, false); 488 | 489 | facebook.oauthRequest = function(host, path, params, callback) { 490 | assert.equal(host, 'graph.facebook.com'); 491 | assert.equal(path, '/oauth/access_token'); 492 | assert.equal(params.client_id, config.appId); 493 | assert.equal(params.client_secret, config.secret); 494 | assert.equal(params.redirect_uri, 'http://example.com/'); 495 | assert.equal(params.code, 'dummy'); 496 | callback(new Error('test'), null); 497 | }; 498 | facebook.getAccessTokenFromCode('dummy', 'http://example.com/', function(err, response) { 499 | assert.ok(err instanceof Error); 500 | assert.equal(err.message, 'test'); 501 | assert.equal(response, null); 502 | 503 | facebook.oauthRequest = function(host, path, params, callback) { 504 | callback(new BaseFacebook.FacebookApiError({}), null); 505 | }; 506 | facebook.getAccessTokenFromCode('dummy', 'http://example.com/', function(err, response) { 507 | assert.equal(err, null); 508 | assert.equal(response, false); 509 | 510 | facebook.oauthRequest = function(host, path, params, callback) { 511 | callback(null, {}); 512 | }; 513 | facebook.getAccessTokenFromCode('dummy', 'http://example.com/', function(err, response) { 514 | assert.equal(err, null); 515 | assert.equal(response, false); 516 | 517 | facebook.oauthRequest = function(host, path, params, callback) { 518 | callback(null, { access_token: 'test_access_token' }); 519 | }; 520 | facebook.getAccessTokenFromCode('dummy', 'http://example.com/', function(err, response) { 521 | assert.equal(err, null); 522 | assert.equal(response, false); 523 | 524 | facebook.oauthRequest = function(host, path, params, callback) { 525 | callback(null, 'access_token=dummy-access-token&expires=3600'); 526 | }; 527 | facebook.getAccessTokenFromCode('dummy', 'http://example.com/', function(err, response) { 528 | assert.equal(err, null); 529 | assert.equal(response, 'dummy-access-token'); 530 | done = true; 531 | }); 532 | }); 533 | }); 534 | }); 535 | }); 536 | }); 537 | }); 538 | }, 539 | 540 | apiForLoggedOutUsers: function(beforeExit, assert) { 541 | var done = false; 542 | beforeExit(function() { assert.ok(done) }); 543 | var facebook = new TransientFacebook({ 544 | appId: config.appId, 545 | secret: config.secret 546 | }); 547 | 548 | facebook.api({ method: 'fql.query', query: 'SELECT name FROM user WHERE uid=4' }, function(err, response) { 549 | assert.equal(err, null); 550 | assert.ok(isArray(response)); 551 | assert.equal(response.length, 1, 'Expect one row back.'); 552 | assert.equal(response[0].name, 'Mark Zuckerberg', 'Expect the name back.'); 553 | done = true; 554 | }); 555 | }, 556 | 557 | apiWithBogusAccessToken: function(beforeExit, assert) { 558 | var done = false; 559 | beforeExit(function() { assert.ok(done) }); 560 | var facebook = new TransientFacebook({ 561 | appId: config.appId, 562 | secret: config.secret 563 | }); 564 | 565 | facebook.setAccessToken('this-is-not-really-an-access-token'); 566 | // if we don't set an access token and there's no way to 567 | // get one, then the FQL query below works beautifully, handing 568 | // over Zuck's public data. But if you specify a bogus access 569 | // token as I have right here, then the FQL query should fail. 570 | // We could return just Zuck's public data, but that wouldn't 571 | // advertise the issue that the access token is at worst broken 572 | // and at best expired. 573 | facebook.api({ method: 'fql.query', query: 'SELECT name FROM profile WHERE id=4' }, function(err, response) { 574 | assert.notEqual(null, err); 575 | var result = err.getResult(); 576 | assert.ok(result && typeof result === 'object', 'expect a result object'); 577 | assert.equal('190', result.error_code, 'expect code') 578 | done = true; 579 | }); 580 | }, 581 | 582 | apiGraphPublicData: function(beforeExit, assert) { 583 | var done = false; 584 | beforeExit(function() { assert.ok(done) }); 585 | var facebook = new TransientFacebook({ 586 | appId: config.appId, 587 | secret: config.secret 588 | }); 589 | 590 | facebook.api('/jerry', function(err, response) { 591 | assert.equal(err, null); 592 | assert.equal(response.id, '214707', 'should get expected id.'); 593 | done = true; 594 | }); 595 | }, 596 | 597 | graphAPIWithBogusAccessToken: function(beforeExit, assert) { 598 | var done = false; 599 | beforeExit(function() { assert.ok(done) }); 600 | var facebook = new TransientFacebook({ 601 | appId: config.appId, 602 | secret: config.secret 603 | }); 604 | 605 | facebook.setAccessToken('this-is-not-really-an-access-token'); 606 | facebook.api('/me', function(err, response) { 607 | assert.equal(response, null); 608 | assert.notEqual(err, null); 609 | assert.equal(err + '', 'OAuthException: Invalid OAuth access token.', 'Expect the invalid OAuth token message.'); 610 | done = true; 611 | }); 612 | }, 613 | 614 | /* 615 | // TODO Create test user and application pattern. 616 | graphAPIWithExpiredAccessToken: function(beforeExit, assert) { 617 | var done = false; 618 | beforeExit(function() { assert.ok(done) }); 619 | var facebook = new TransientFacebook({ 620 | appId: config.appId, 621 | secret: config.secret 622 | }); 623 | 624 | facebook.setAccessToken('TODO'); 625 | facebook.api('/me', function(err, response) { 626 | assert.equal(response, null); 627 | assert.notEqual(err, null); 628 | assert.equal(err + '', 'OAuthException: Error validating access token:', 'Expect the invalid OAuth token message.'); 629 | done = true; 630 | }); 631 | }, 632 | 633 | graphAPIMethod: function(beforeExit, assert) { 634 | var done = false; 635 | beforeExit(function() { assert.ok(done) }); 636 | var facebook = new TransientFacebook({ 637 | appId: config.appId, 638 | secret: config.secret 639 | }); 640 | 641 | facebook.api('/amachang', 'DELETE', function(err, response) { 642 | console.log(err, response); 643 | assert.equal(response, null); 644 | assert.notEqual(err, null); 645 | assert.equal(err + '', 646 | 'OAuthException: A user access token is required to request this resource.', 647 | 'Expect the invalid session message.'); 648 | done = true; 649 | }); 650 | }, 651 | 652 | graphAPIOAuthSpecError: function(beforeExit, assert) { 653 | var done = false; 654 | beforeExit(function() { assert.ok(done) }); 655 | var facebook = new TransientFacebook({ 656 | appId: config.migratedAppId, 657 | secret: config.migratedSecret 658 | }); 659 | 660 | facebook.api('/me', { client_id: config.migratedAppId }, function(err, response) { 661 | assert.equal(response, null); 662 | assert.notEqual(err, null); 663 | assert.equal(err + '', 664 | 'invalid_request: An active access token must be used to query information about the current user.', 665 | 'Expect the invalid session message.'); 666 | done = true; 667 | }); 668 | }, 669 | 670 | graphAPIMethodOAuthSpecError: function(beforeExit, assert) { 671 | var done = false; 672 | beforeExit(function() { assert.ok(done) }); 673 | var facebook = new TransientFacebook({ 674 | appId: config.migratedAppId, 675 | secret: config.migratedSecret 676 | }); 677 | 678 | facebook.api('/daaku.shah', 'DELETE', { client_id: config.migratedAppId }, function(err, response) { 679 | assert.equal(response, null); 680 | assert.notEqual(err, null); 681 | assert.equal((err + '').indexOf('invalid_request'), 0); 682 | done = true; 683 | }); 684 | }, 685 | */ 686 | 687 | graphAPIWithOnlyParams: function(beforeExit, assert) { 688 | var done = false; 689 | beforeExit(function() { assert.ok(done) }); 690 | var facebook = new TransientFacebook({ 691 | appId: config.appId, 692 | secret: config.secret 693 | }); 694 | facebook.api('/jerry', function(err, response) { 695 | assert.equal(err, null); 696 | assert.notEqual(response, null); 697 | assert.ok(response.hasOwnProperty('id'), 'User ID should be public.'); 698 | assert.ok(response.hasOwnProperty('name'), 'User\'s name should be public.'); 699 | assert.ok(response.hasOwnProperty('first_name'), 'User\'s first name should be public.'); 700 | assert.ok(response.hasOwnProperty('last_name'), 'User\'s last name should be public.'); 701 | assert.equal(response.hasOwnProperty('work'), false, 702 | 'User\'s work history should only be available with ' + 703 | 'a valid access token.'); 704 | assert.equal(response.hasOwnProperty('education'), false, 705 | 'User\'s education history should only be ' + 706 | 'available with a valid access token.'); 707 | assert.equal(response.hasOwnProperty('verified'), false, 708 | 'User\'s verification status should only be ' + 709 | 'available with a valid access token.'); 710 | done = true; 711 | }); 712 | }, 713 | 714 | loginURLDefaults: function(beforeExit, assert) { 715 | var done = false; 716 | beforeExit(function() { assert.ok(done) }); 717 | var facebook = new TransientFacebook({ 718 | appId: config.appId, 719 | secret: config.secret, 720 | request: { 721 | connection: { 722 | }, 723 | headers: { 724 | host: 'fbrell.com' 725 | }, 726 | url: '/examples' 727 | } 728 | }); 729 | var encodedUrl = encodeURIComponent('http://fbrell.com/examples'); 730 | assert.notEqual(facebook.getLoginUrl().indexOf(encodedUrl), -1, 731 | 'Expect the current url to exist.'); 732 | done = true; 733 | }, 734 | 735 | loginURLDefaultsDropStateQueryParam: function(beforeExit, assert) { 736 | var done = false; 737 | beforeExit(function() { assert.ok(done) }); 738 | var facebook = new TransientFacebook({ 739 | appId: config.appId, 740 | secret: config.secret, 741 | request: { 742 | connection: { 743 | }, 744 | headers: { 745 | host: 'fbrell.com' 746 | }, 747 | url: '/examples?state=xx42xx' 748 | } 749 | }); 750 | 751 | var expectEncodedUrl = encodeURIComponent('http://fbrell.com/examples'); 752 | assert.notEqual(facebook.getLoginUrl().indexOf(expectEncodedUrl), -1, 753 | 'Expect the current url to exist.'); 754 | assert.equal(facebook.getLoginUrl().indexOf('xx42xx'), -1, 'Expect the session param to be dropped.'); 755 | done = true; 756 | }, 757 | 758 | loginURLDefaultsDropCodeQueryParam: function(beforeExit, assert) { 759 | var done = false; 760 | beforeExit(function() { assert.ok(done) }); 761 | var facebook = new TransientFacebook({ 762 | appId: config.appId, 763 | secret: config.secret, 764 | request: { 765 | connection: { 766 | }, 767 | headers: { 768 | host: 'fbrell.com' 769 | }, 770 | url: '/examples?code=xx42xx' 771 | } 772 | }); 773 | 774 | var expectEncodedUrl = encodeURIComponent('http://fbrell.com/examples'); 775 | assert.notEqual(facebook.getLoginUrl().indexOf(expectEncodedUrl), -1, 'Expect the current url to exist.'); 776 | assert.equal(facebook.getLoginUrl().indexOf('xx42xx'), -1, 'Expect the session param to be dropped.'); 777 | done = true; 778 | }, 779 | 780 | loginURLDefaultsDropSignedRequestParamButNotOthers: function(beforeExit, assert) { 781 | var done = false; 782 | beforeExit(function() { assert.ok(done) }); 783 | var facebook = new TransientFacebook({ 784 | appId: config.appId, 785 | secret: config.secret, 786 | request: { 787 | connection: { 788 | }, 789 | headers: { 790 | host: 'fbrell.com' 791 | }, 792 | url: '/examples?signed_request=xx42xx&do_not_drop=xx43xx' 793 | } 794 | }); 795 | 796 | var expectEncodedUrl = encodeURIComponent('http://fbrell.com/examples'); 797 | assert.equal(facebook.getLoginUrl().indexOf('xx42xx'), -1, 'Expect the session param to be dropped.'); 798 | assert.notEqual(facebook.getLoginUrl().indexOf('xx43xx'), -1, 'Expect the do_not_drop param to exist.'); 799 | done = true; 800 | }, 801 | 802 | testLoginURLCustomNext: function(beforeExit, assert) { 803 | var done = false; 804 | beforeExit(function() { assert.ok(done) }); 805 | var facebook = new TransientFacebook({ 806 | appId: config.appId, 807 | secret: config.secret, 808 | request: { 809 | connection: { 810 | }, 811 | headers: { 812 | host: 'fbrell.com' 813 | }, 814 | url: '/examples' 815 | } 816 | }); 817 | 818 | var next = 'http://fbrell.com/custom'; 819 | var loginUrl = facebook.getLoginUrl({ 820 | redirect_uri: next, 821 | cancel_url: next 822 | }); 823 | 824 | var currentEncodedUrl = encodeURIComponent('http://fbrell.com/examples'); 825 | var expectedEncodedUrl = encodeURIComponent(next); 826 | assert.notEqual(loginUrl.indexOf(expectedEncodedUrl), -1); 827 | assert.equal(loginUrl.indexOf(currentEncodedUrl), -1); 828 | done = true; 829 | }, 830 | 831 | logoutURLDefaults: function(beforeExit, assert) { 832 | var done = false; 833 | beforeExit(function() { assert.ok(done) }); 834 | var facebook = new TransientFacebook({ 835 | appId: config.appId, 836 | secret: config.secret, 837 | request: { 838 | connection: { 839 | }, 840 | headers: { 841 | host: 'fbrell.com' 842 | }, 843 | url: '/examples' 844 | } 845 | }); 846 | 847 | var encodedUrl = encodeURIComponent('http://fbrell.com/examples'); 848 | facebook.getLogoutUrl(function(err, url) { 849 | assert.equal(err, null); 850 | assert.notEqual(url.indexOf(encodedUrl), -1, 'Expect the current url to exist.'); 851 | 852 | var facebook = new TransientFacebook({ 853 | appId: 'dummy', 854 | secret: 'dummy' 855 | }); 856 | facebook.getUserAccessToken = function(callback) { 857 | callback(new Error('dummy-error'), null); 858 | }; 859 | facebook.getLogoutUrl(function(err, url) { 860 | assert.notEqual(err, null); 861 | assert.equal(err.message, 'dummy-error'); 862 | assert.equal(url, null); 863 | 864 | var facebook = new TransientFacebook({ 865 | appId: 'dummy', 866 | secret: 'dummy' 867 | }); 868 | facebook.getLogoutUrl(function(err, url) { 869 | assert.notEqual(err, null); 870 | assert.equal(err.message, 'No request object.'); 871 | assert.equal(url, null); 872 | done = true; 873 | }); 874 | }); 875 | }); 876 | }, 877 | 878 | loginStatusURLDefaults: function(beforeExit, assert) { 879 | var done = false; 880 | beforeExit(function() { assert.ok(done) }); 881 | var facebook = new TransientFacebook({ 882 | appId: config.appId, 883 | secret: config.secret, 884 | request: { 885 | connection: { 886 | }, 887 | headers: { 888 | host: 'fbrell.com' 889 | }, 890 | url: '/examples' 891 | } 892 | }); 893 | 894 | var encodedUrl = encodeURIComponent('http://fbrell.com/examples'); 895 | assert.notEqual(facebook.getLoginStatusUrl().indexOf(encodedUrl), -1, 896 | 'Expect the current url to exist.'); 897 | done = true; 898 | }, 899 | 900 | loginStatusURLCustom: function(beforeExit, assert) { 901 | var done = false; 902 | beforeExit(function() { assert.ok(done) }); 903 | var facebook = new TransientFacebook({ 904 | appId: config.appId, 905 | secret: config.secret, 906 | request: { 907 | connection: { 908 | }, 909 | headers: { 910 | host: 'fbrell.com' 911 | }, 912 | url: '/examples' 913 | } 914 | }); 915 | 916 | var encodedUrl1 = encodeURIComponent('http://fbrell.com/examples'); 917 | var okUrl = 'http://fbrell.com/here1'; 918 | var encodedUrl2 = encodeURIComponent(okUrl); 919 | var loginStatusUrl = facebook.getLoginStatusUrl({ 920 | ok_session: okUrl 921 | }); 922 | assert.notEqual(loginStatusUrl.indexOf(encodedUrl1), -1, 'Expect the current url to exist.'); 923 | assert.notEqual(loginStatusUrl.indexOf(encodedUrl2), -1, 'Expect the current url to exist.'); 924 | done = true; 925 | }, 926 | 927 | nonDefaultPort: function(beforeExit, assert) { 928 | var done = false; 929 | beforeExit(function() { assert.ok(done) }); 930 | var facebook = new TransientFacebook({ 931 | appId: config.appId, 932 | secret: config.secret, 933 | request: { 934 | connection: { 935 | }, 936 | headers: { 937 | host: 'fbrell.com:8080' 938 | }, 939 | url: '/examples' 940 | } 941 | }); 942 | 943 | var encodedUrl = encodeURIComponent('http://fbrell.com:8080/examples'); 944 | assert.notEqual(facebook.getLoginUrl().indexOf(encodedUrl), -1, 'Expect the current url to exist.'); 945 | done = true; 946 | }, 947 | 948 | secureCurrentUrl: function(beforeExit, assert) { 949 | var done = false; 950 | beforeExit(function() { assert.ok(done) }); 951 | var facebook = new TransientFacebook({ 952 | appId: config.appId, 953 | secret: config.secret, 954 | request: { 955 | connection: { 956 | pair: {} 957 | }, 958 | headers: { 959 | host: 'fbrell.com' 960 | }, 961 | url: '/examples' 962 | } 963 | }); 964 | var encodedUrl = encodeURIComponent('https://fbrell.com/examples'); 965 | assert.notEqual(facebook.getLoginUrl().indexOf(encodedUrl), -1, 'Expect the current url to exist.'); 966 | done = true; 967 | }, 968 | 969 | secureCurrentUrlWithNonDefaultPort: function(beforeExit, assert) { 970 | var done = false; 971 | beforeExit(function() { assert.ok(done) }); 972 | var facebook = new TransientFacebook({ 973 | appId: config.appId, 974 | secret: config.secret, 975 | request: { 976 | connection: { 977 | pair: {} 978 | }, 979 | headers: { 980 | host: 'fbrell.com:8080' 981 | }, 982 | url: '/examples' 983 | } 984 | }); 985 | var encodedUrl = encodeURIComponent('https://fbrell.com:8080/examples'); 986 | assert.notEqual(facebook.getLoginUrl().indexOf(encodedUrl), -1, 'Expect the current url to exist.'); 987 | done = true; 988 | }, 989 | 990 | /* 991 | appSecretCall: function(beforeExit, assert) { 992 | var done = false; 993 | beforeExit(function() { assert.ok(done) }); 994 | var facebook = new TransientFacebook({ 995 | appId: config.appId, 996 | secret: config.secret 997 | }); 998 | 999 | var properExceptionThrown = false; 1000 | var self = this; 1001 | facebook.api('/' + config.appId + '/insights', function(err, response) { 1002 | assert.notEqual(err, null); 1003 | assert.equal(response, null); 1004 | assert.notEqual(err.message.indexOf('Requires session when calling from a desktop app'), -1, 1005 | 'Incorrect exception type thrown when trying to gain ' + 1006 | 'insights for desktop app without a user access token.'); 1007 | done = true; 1008 | }); 1009 | }, 1010 | */ 1011 | 1012 | base64UrlEncode: function(beforeExit, assert) { 1013 | var done = false; 1014 | beforeExit(function() { assert.ok(done) }); 1015 | var facebook = new TransientFacebook({ 1016 | appId: config.appId, 1017 | secret: config.secret 1018 | }); 1019 | var input = 'Facebook rocks'; 1020 | var output = 'RmFjZWJvb2sgcm9ja3M'; 1021 | 1022 | assert.equal(facebook.base64UrlDecode(output).toString('utf-8'), input); 1023 | done = true; 1024 | }, 1025 | 1026 | signedToken: function(beforeExit, assert) { 1027 | var done = false; 1028 | beforeExit(function() { assert.ok(done) }); 1029 | var facebook = new TransientFacebook({ 1030 | appId: '117743971608120', 1031 | secret: '943716006e74d9b9283d4d5d8ab93204' 1032 | }); 1033 | var payload = facebook.parseSignedRequest('1sxR88U4SW9m6QnSxwCEw_CObqsllXhnpP5j2pxD97c.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsImV4cGlyZXMiOjEyODEwNTI4MDAsIm9hdXRoX3Rva2VuIjoiMTE3NzQzOTcxNjA4MTIwfDIuVlNUUWpub3hYVVNYd1RzcDB1U2g5d19fLjg2NDAwLjEyODEwNTI4MDAtMTY3Nzg0NjM4NXx4NURORHBtcy1nMUM0dUJHQVYzSVdRX2pYV0kuIiwidXNlcl9pZCI6IjE2Nzc4NDYzODUifQ'); 1034 | assert.notEqual(payload, null, 'Expected token to parse'); 1035 | assert.equal(facebook.getSignedRequest(), null); 1036 | 1037 | facebook.request = { 1038 | body: { 1039 | signed_request: '1sxR88U4SW9m6QnSxwCEw_CObqsllXhnpP5j2pxD97c.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsImV4cGlyZXMiOjEyODEwNTI4MDAsIm9hdXRoX3Rva2VuIjoiMTE3NzQzOTcxNjA4MTIwfDIuVlNUUWpub3hYVVNYd1RzcDB1U2g5d19fLjg2NDAwLjEyODEwNTI4MDAtMTY3Nzg0NjM4NXx4NURORHBtcy1nMUM0dUJHQVYzSVdRX2pYV0kuIiwidXNlcl9pZCI6IjE2Nzc4NDYzODUifQ' 1040 | } 1041 | }; 1042 | assert.deepEqual(facebook.getSignedRequest(), payload); 1043 | 1044 | var facebook = new TransientFacebook({ 1045 | appId: '117743971608120', 1046 | secret: '943716006e74d9b9283d4d5d8ab93204', 1047 | request: { 1048 | body: { 1049 | signed_request: 'dummy' 1050 | } 1051 | } 1052 | }); 1053 | facebook.errorLog = function() {}; 1054 | assert.equal(facebook.getSignedRequest(), null); 1055 | 1056 | done = true; 1057 | }, 1058 | 1059 | nonTossedSignedToken: function(beforeExit, assert) { 1060 | var done = false; 1061 | beforeExit(function() { assert.ok(done) }); 1062 | var facebook = new TransientFacebook({ 1063 | appId: '117743971608120', 1064 | secret: '943716006e74d9b9283d4d5d8ab93204' 1065 | }); 1066 | var payload = facebook.parseSignedRequest('c0Ih6vYvauDwncv0n0pndr0hP0mvZaJPQDPt6Z43O0k.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiJ9'); 1067 | assert.notEqual(payload, null, 'Expected token to parse'); 1068 | assert.equal(facebook.getSignedRequest(), null); 1069 | 1070 | facebook.request = { 1071 | body: { 1072 | signed_request: 'c0Ih6vYvauDwncv0n0pndr0hP0mvZaJPQDPt6Z43O0k.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiJ9' 1073 | } 1074 | }; 1075 | assert.deepEqual(facebook.getSignedRequest(), { algorithm: 'HMAC-SHA256' }); 1076 | done = true; 1077 | }, 1078 | 1079 | nonGeneralSignedToken: function(beforeExit, assert) { 1080 | var done = false; 1081 | beforeExit(function() { assert.ok(done) }); 1082 | var facebook = new TransientFacebook({ 1083 | appId: config.appId, 1084 | secret: 'secret-dummy' 1085 | }); 1086 | var data = facebook.parseSignedRequest('2mYrTJ6TkHRZ1iLlFFt3He30-e5cSgvN5U9COCqoPvE.eyAiaG9nZSI6ICJmdWdhIiwgImFsZ29yaXRobSI6ICJITUFDLVNIQTI1NiIgfQ'); 1087 | assert.equal(data.hoge, 'fuga'); 1088 | assert.equal(data.algorithm, 'HMAC-SHA256'); 1089 | done = true; 1090 | }, 1091 | 1092 | /* 1093 | public function testBundledCACert() { 1094 | $facebook = new TransientFacebook(array( 1095 | 'appId' => self::APP_ID, 1096 | 'secret' => self::SECRET 1097 | )); 1098 | 1099 | // use the bundled cert from the start 1100 | Facebook::$CURL_OPTS[CURLOPT_CAINFO] = 1101 | dirname(__FILE__) . '/../src/fb_ca_chain_bundle.crt'; 1102 | $response = $facebook->api('/naitik'); 1103 | 1104 | unset(Facebook::$CURL_OPTS[CURLOPT_CAINFO]); 1105 | $this->assertEquals( 1106 | $response['id'], '5526183', 'should get expected id.'); 1107 | } 1108 | */ 1109 | 1110 | videoUpload: function(beforeExit, assert) { 1111 | var done = false; 1112 | beforeExit(function() { assert.ok(done) }); 1113 | var facebook = new TransientFacebook({ 1114 | appId: config.appId, 1115 | secret: config.secret 1116 | }); 1117 | 1118 | var host = null; 1119 | facebook.makeRequest = function(_host, path, params, callback) { 1120 | host = _host; 1121 | callback(null, null); 1122 | }; 1123 | facebook.api({ method: 'video.upload' }, function(err, response) { 1124 | assert.equal(host, 'api-video.facebook.com', 'video.upload should go against api-video'); 1125 | done = true; 1126 | }); 1127 | }, 1128 | 1129 | getUserAndAccessTokenFromSession: function(beforeExit, assert) { 1130 | var done = false; 1131 | beforeExit(function() { assert.ok(done) }); 1132 | var facebook = new TransientFacebook({ 1133 | appId: config.appId, 1134 | secret: config.secret 1135 | }); 1136 | 1137 | facebook.setPersistentData('access_token', 'foobar'); 1138 | facebook.setPersistentData('user_id', '12345'); 1139 | facebook.getAccessToken(function(err, accessToken) { 1140 | assert.equal('foobar', accessToken, 'Get access token from persistent store.'); 1141 | facebook.getUser(function(err, user) { 1142 | assert.equal('12345', user, 'Get user id from persistent store.'); 1143 | facebook.getUser(function(err, user) { 1144 | assert.equal('12345', user, 'Get user again'); 1145 | done = true; 1146 | }); 1147 | }); 1148 | }); 1149 | }, 1150 | 1151 | getUserAndAccessTokenFromSignedRequestNotSession: function(beforeExit, assert) { 1152 | var done = false; 1153 | beforeExit(function() { assert.ok(done) }); 1154 | var facebook = new TransientFacebook({ 1155 | appId: '117743971608120', 1156 | secret: '943716006e74d9b9283d4d5d8ab93204', 1157 | request: { 1158 | query: { 1159 | signed_request: '1sxR88U4SW9m6QnSxwCEw_CObqsllXhnpP5j2pxD97c.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsImV4cGlyZXMiOjEyODEwNTI4MDAsIm9hdXRoX3Rva2VuIjoiMTE3NzQzOTcxNjA4MTIwfDIuVlNUUWpub3hYVVNYd1RzcDB1U2g5d19fLjg2NDAwLjEyODEwNTI4MDAtMTY3Nzg0NjM4NXx4NURORHBtcy1nMUM0dUJHQVYzSVdRX2pYV0kuIiwidXNlcl9pZCI6IjE2Nzc4NDYzODUifQ' 1160 | } 1161 | } 1162 | }); 1163 | 1164 | facebook.setPersistentData('user_id', '41572'); 1165 | facebook.setPersistentData('access_token', 'dummy'); 1166 | facebook.getUser(function(err, user) { 1167 | assert.equal(err, null); 1168 | assert.notEqual('41572', user, 'Got user from session instead of signed request.'); 1169 | assert.equal('1677846385', user, 'Failed to get correct user ID from signed request.'); 1170 | facebook.getAccessToken(function(err, accessToken) { 1171 | assert.equal(err, null); 1172 | assert.notEqual(accessToken, 'dummy', 1173 | 'Got access token from session instead of signed request.'); 1174 | assert.notEqual(accessToken.length, 0, 1175 | 'Failed to extract an access token from the signed request.'); 1176 | done = true; 1177 | }); 1178 | }); 1179 | }, 1180 | 1181 | getUserWithoutCodeOrSignedRequestOrSession: function(beforeExit, assert) { 1182 | var done = false; 1183 | beforeExit(function() { assert.ok(done) }); 1184 | var facebook = new TransientFacebook({ 1185 | appId: config.appId, 1186 | secret: config.secret 1187 | }); 1188 | 1189 | facebook.getUser(function(err, user) { 1190 | assert.equal(user, 0); 1191 | done = true; 1192 | }); 1193 | }, 1194 | 1195 | getUserWithAvailableDataError: function(beforeExit, assert) { 1196 | var done = false; 1197 | beforeExit(function() { assert.ok(done) }); 1198 | 1199 | var facebook = new TransientFacebook({ 1200 | appId: config.appId, 1201 | secret: config.secret 1202 | }); 1203 | 1204 | facebook.getUserFromAvailableData = function(callback) { 1205 | callback(new Error('dummy'), null); 1206 | }; 1207 | facebook.getUser(function(err, user) { 1208 | assert.notEqual(err, null); 1209 | assert.equal(err.message, 'dummy'); 1210 | assert.equal(user, null); 1211 | done = true; 1212 | }); 1213 | }, 1214 | 1215 | getAccessTokenWithUserAccessTokenError: function(beforeExit, assert) { 1216 | var done = false; 1217 | beforeExit(function() { assert.ok(done) }); 1218 | var facebook = new TransientFacebook({ 1219 | appId: 'dummy', 1220 | secret: 'secret-dummy' 1221 | }); 1222 | facebook.getUserAccessToken = function(callback) { 1223 | callback(new Error('dummy-error'), null); 1224 | }; 1225 | facebook.getAccessToken(function(err, accessToken) { 1226 | assert.notEqual(err, null); 1227 | assert.equal(err.message, 'dummy-error'); 1228 | assert.equal(accessToken, null); 1229 | done = true; 1230 | }); 1231 | }, 1232 | 1233 | getUserAccessToken: function(beforeExit, assert) { 1234 | var done = false; 1235 | beforeExit(function() { assert.ok(done) }); 1236 | var facebook = new TransientFacebook({ 1237 | appId: 'dummy', 1238 | secret: 'secret-dummy', 1239 | request: { 1240 | connection: {}, 1241 | headers: {}, 1242 | body: { 1243 | signed_request: '0GCZT4MghxPvJ7dDH84rLxeNp01h5FstqDVKuBHHkH8.eyAiY29kZSI6ICJkdW1teSIsICJhbGdvcml0aG0iOiAiSE1BQy1TSEEyNTYiIH0' 1244 | } 1245 | } 1246 | }); 1247 | facebook.getAccessTokenFromCode = function(code, redirectUri, callback) { 1248 | assert.equal(code, 'dummy'); 1249 | callback(new Error('test'), null); 1250 | }; 1251 | facebook.getUserAccessToken(function(err, accessToken) { 1252 | assert.ok(err instanceof Error); 1253 | assert.equal(err.message, 'test'); 1254 | assert.equal(accessToken, null); 1255 | assert.equal(facebook.getPersistentData('code'), false); 1256 | assert.equal(facebook.getPersistentData('access_token'), false); 1257 | 1258 | facebook.getAccessTokenFromCode = function(code, redirectUri, callback) { 1259 | callback(null, 'dummy-access-token'); 1260 | }; 1261 | facebook.getUserAccessToken(function(err, accessToken) { 1262 | assert.equal(err, null); 1263 | assert.equal(accessToken, 'dummy-access-token'); 1264 | assert.equal(facebook.getPersistentData('code'), 'dummy'); 1265 | assert.equal(facebook.getPersistentData('access_token'), 'dummy-access-token'); 1266 | 1267 | facebook.getAccessTokenFromCode = function(code, redirectUri, callback) { 1268 | callback(null, false); 1269 | }; 1270 | 1271 | facebook.getUserAccessToken(function(err, accessToken) { 1272 | assert.equal(err, null); 1273 | assert.equal(accessToken, false); 1274 | assert.equal(facebook.getPersistentData('code'), false); 1275 | assert.equal(facebook.getPersistentData('access_token'), false); 1276 | 1277 | facebook = new TransientFacebook({ 1278 | appId: 'dummy', 1279 | secret: 'secret-dummy', 1280 | request: { 1281 | connection: {}, 1282 | headers: {}, 1283 | body: { 1284 | signed_request: 'Sy3mhK4xP9RWsN905MP1sJrkbkGrXgz2y7r-Fx6lqBU.eyAiYWxnb3JpdGhtIjogIkhNQUMtU0hBMjU2IiB9' 1285 | } 1286 | } 1287 | }); 1288 | 1289 | facebook.setPersistentData('code', 'bad data'); 1290 | facebook.setPersistentData('access_token', 'bad data'); 1291 | facebook.getUserAccessToken(function(err, accessToken) { 1292 | assert.equal(err, null); 1293 | assert.equal(accessToken, false); 1294 | assert.equal(facebook.getPersistentData('code'), false); 1295 | assert.equal(facebook.getPersistentData('access_token'), false); 1296 | 1297 | facebook = new TransientFacebook({ 1298 | appId: 'dummy', 1299 | secret: 'secret-dummy', 1300 | request: { 1301 | connection: {}, 1302 | headers: {}, 1303 | query: { 1304 | code: 'dummy-code', 1305 | state: 'dummy-state' 1306 | } 1307 | }, 1308 | store: { 1309 | state: 'dummy-state' 1310 | } 1311 | }); 1312 | 1313 | var responseReturned = false; 1314 | facebook.getAccessTokenFromCode = function(code, redirectUri, callback) { 1315 | assert.equal(err, null); 1316 | assert.equal(code, 'dummy-code'); 1317 | responseReturned = true; 1318 | callback(null, false); 1319 | }; 1320 | facebook.getUserAccessToken(function(err, accessToken) { 1321 | assert.equal(err, null); 1322 | assert.equal(accessToken, false); 1323 | assert.ok(responseReturned); 1324 | done = true; 1325 | }); 1326 | }); 1327 | }); 1328 | }); 1329 | }); 1330 | }, 1331 | 1332 | getUserFromAvailableData: function(beforeExit, assert) { 1333 | var done = false; 1334 | beforeExit(function() { assert.ok(done) }); 1335 | 1336 | var facebook = new TransientFacebook({ 1337 | appId: 'dummy', 1338 | secret: 'secret-dummy', 1339 | request: { 1340 | connection: {}, 1341 | headers: {}, 1342 | body: { 1343 | signed_request: 'Sy3mhK4xP9RWsN905MP1sJrkbkGrXgz2y7r-Fx6lqBU.eyAiYWxnb3JpdGhtIjogIkhNQUMtU0hBMjU2IiB9' 1344 | } 1345 | } 1346 | }); 1347 | facebook.getUserFromAvailableData(function(err, user) { 1348 | assert.equal(err, null); 1349 | assert.equal(user, 0); 1350 | 1351 | facebook = new TransientFacebook({ 1352 | appId: config.appId, 1353 | secret: config.secret, 1354 | request: { 1355 | connection: {}, 1356 | headers: {}, 1357 | body: {} 1358 | } 1359 | }); 1360 | 1361 | facebook.getAccessToken = function(callback) { 1362 | callback(new Error('test'), null); 1363 | }; 1364 | facebook.getUserFromAvailableData(function(err, user) { 1365 | assert.ok(err instanceof Error); 1366 | assert.equal(user, null); 1367 | assert.equal(err.message, 'test'); 1368 | 1369 | facebook = new TransientFacebook({ 1370 | appId: config.appId, 1371 | secret: config.secret, 1372 | request: { 1373 | connection: {}, 1374 | headers: {}, 1375 | body: {} 1376 | }, 1377 | store: { 1378 | access_token: 'dummy' 1379 | } 1380 | }); 1381 | 1382 | var called = false; 1383 | facebook.api = function(method, callback) { 1384 | assert.equal(method, '/me'); 1385 | called = true; 1386 | callback(new Error('test'), null); 1387 | }; 1388 | facebook.getUserFromAvailableData(function(err, user) { 1389 | assert.ok(called); 1390 | assert.equal(err, null); 1391 | assert.equal(user, 0); 1392 | 1393 | var appToken = facebook.getApplicationAccessToken(); 1394 | 1395 | facebook = new TransientFacebook({ 1396 | appId: config.appId, 1397 | secret: config.secret, 1398 | request: { 1399 | connection: {}, 1400 | headers: {}, 1401 | body: {} 1402 | }, 1403 | store: { 1404 | access_token: appToken 1405 | } 1406 | }); 1407 | 1408 | var called0 = false; 1409 | facebook.api = function(method, callback) { 1410 | called0 = true; 1411 | callback(new Error('test'), null); 1412 | }; 1413 | facebook.getUserFromAvailableData(function(err, user) { 1414 | assert.equal(called0, false); 1415 | assert.equal(err, null); 1416 | assert.equal(user, 0); 1417 | 1418 | facebook = new TransientFacebook({ 1419 | appId: config.appId, 1420 | secret: config.secret, 1421 | request: { 1422 | connection: {}, 1423 | headers: {}, 1424 | body: {} 1425 | }, 1426 | store: { 1427 | access_token: 'dummy_access_token' 1428 | } 1429 | }); 1430 | facebook.getUserFromAccessToken = function(callback) { 1431 | callback(new Error('error-message'), null); 1432 | }; 1433 | facebook.getUserFromAvailableData(function(err, user) { 1434 | assert.notEqual(err, null); 1435 | assert.equal(err.message, 'error-message'); 1436 | assert.equal(user, null); 1437 | 1438 | facebook.getUserFromAccessToken = function(callback) { 1439 | callback(null, '1323'); 1440 | }; 1441 | facebook.getUserFromAvailableData(function(err, user) { 1442 | assert.equal(err, null); 1443 | assert.equal(user, '1323'); 1444 | done = true; 1445 | }); 1446 | }); 1447 | }); 1448 | }); 1449 | }); 1450 | }); 1451 | }, 1452 | 1453 | isVideoPost: function(beforeExit, assert) { 1454 | var done = false; 1455 | beforeExit(function() { assert.ok(done) }); 1456 | 1457 | var facebook = new TransientFacebook({ 1458 | appId: config.appId, 1459 | secret: config.secret 1460 | }); 1461 | 1462 | assert.equal(facebook.isVideoPost('/me/videos'), false); 1463 | assert.equal(facebook.isVideoPost('/foo/videos', 'GET'), false); 1464 | assert.equal(facebook.isVideoPost('/bar/videos', 'POST'), true); 1465 | assert.equal(facebook.isVideoPost('/me/videossss', 'POST'), false); 1466 | assert.equal(facebook.isVideoPost('/videos', 'POST'), false); 1467 | assert.equal(facebook.isVideoPost('/baz', 'POST'), false); 1468 | 1469 | done = true; 1470 | }, 1471 | 1472 | requestToGraphVideoDomain: function(beforeExit, assert) { 1473 | var done = false; 1474 | beforeExit(function() { assert.ok(done) }); 1475 | 1476 | var facebook = new TransientFacebook({ 1477 | appId: config.appId, 1478 | secret: config.secret 1479 | }); 1480 | 1481 | facebook.makeRequest = function(host, path, params, callback) { 1482 | assert.equal(host, 'graph-video.facebook.com'); 1483 | callback(null, '{ "test": "ok" }'); 1484 | }; 1485 | 1486 | facebook.graph('/amachang/videos', 'POST', function(err, data) { 1487 | assert.equal(err, null); 1488 | assert.equal(data.test, 'ok'); 1489 | }); 1490 | 1491 | facebook.graph('/foo/videos', 'POST', function(err, data) { 1492 | assert.equal(err, null); 1493 | assert.equal(data.test, 'ok'); 1494 | }); 1495 | 1496 | facebook.makeRequest = function(host, path, params, callback) { 1497 | assert.equal(host, 'graph.facebook.com'); 1498 | callback(null, '{ "test": "ok" }'); 1499 | }; 1500 | 1501 | facebook.graph('/bar/videossss', 'POST', function(err, data) { 1502 | assert.equal(err, null); 1503 | assert.equal(data.test, 'ok'); 1504 | }); 1505 | 1506 | facebook.graph('/videos', 'POST', function(err, data) { 1507 | assert.equal(err, null); 1508 | assert.equal(data.test, 'ok'); 1509 | }); 1510 | 1511 | facebook.graph('/baz/videos', 'GET', function(err, data) { 1512 | assert.equal(err, null); 1513 | assert.equal(data.test, 'ok'); 1514 | }); 1515 | 1516 | done = true; 1517 | }, 1518 | 1519 | graph: function(beforeExit, assert) { 1520 | var done = false; 1521 | beforeExit(function() { assert.ok(done) }); 1522 | 1523 | var facebook = new TransientFacebook({ 1524 | appId: config.appId, 1525 | secret: config.secret 1526 | }); 1527 | 1528 | facebook.graph('/amachang', function(err, data) { 1529 | assert.equal(err, null); 1530 | assert.equal(data.id, '1055572299'); 1531 | facebook.graph('/amachang', 'POST', function(err, data) { 1532 | assert.ok(err instanceof Error); 1533 | assert.equal(data, null); 1534 | facebook.graph('/', { ids: 'amachang,yukoba' }, function(err, data) { 1535 | assert.equal(data.amachang.id, '1055572299'); 1536 | assert.equal(data.yukoba.id, '1192222589'); 1537 | facebook.graph('invalid path', function(err, data) { 1538 | assert.ok(err instanceof Error); 1539 | assert.equal(data, null); 1540 | done = true; 1541 | }); 1542 | }); 1543 | }); 1544 | }); 1545 | }, 1546 | 1547 | destroySession: function(beforeExit, assert) { 1548 | var done = false; 1549 | beforeExit(function() { assert.ok(done) }); 1550 | 1551 | var facebook = new TransientFacebook({ 1552 | appId: '117743971608120', 1553 | secret: '943716006e74d9b9283d4d5d8ab93204', 1554 | request: { 1555 | headers: { 1556 | host: 'www.test.com' 1557 | }, 1558 | cookies: { 1559 | fbsr_117743971608120: '1sxR88U4SW9m6QnSxwCEw_CObqsllXhnpP5j2pxD97c.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsImV4cGlyZXMiOjEyODEwNTI4MDAsIm9hdXRoX3Rva2VuIjoiMTE3NzQzOTcxNjA4MTIwfDIuVlNUUWpub3hYVVNYd1RzcDB1U2g5d19fLjg2NDAwLjEyODEwNTI4MDAtMTY3Nzg0NjM4NXx4NURORHBtcy1nMUM0dUJHQVYzSVdRX2pYV0kuIiwidXNlcl9pZCI6IjE2Nzc4NDYzODUifQ' 1560 | } 1561 | } 1562 | }); 1563 | 1564 | assert.notEqual(facebook.getSignedRequest(), null); 1565 | facebook.destroySession(); 1566 | assert.equal(facebook.getSignedRequest(), null); 1567 | 1568 | var facebook = new TransientFacebook({ 1569 | appId: '117743971608120', 1570 | secret: '943716006e74d9b9283d4d5d8ab93204', 1571 | request: { 1572 | headers: { 1573 | host: 'www.test.com' 1574 | }, 1575 | cookies: { 1576 | fbsr_117743971608120: '1sxR88U4SW9m6QnSxwCEw_CObqsllXhnpP5j2pxD97c.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsImV4cGlyZXMiOjEyODEwNTI4MDAsIm9hdXRoX3Rva2VuIjoiMTE3NzQzOTcxNjA4MTIwfDIuVlNUUWpub3hYVVNYd1RzcDB1U2g5d19fLjg2NDAwLjEyODEwNTI4MDAtMTY3Nzg0NjM4NXx4NURORHBtcy1nMUM0dUJHQVYzSVdRX2pYV0kuIiwidXNlcl9pZCI6IjE2Nzc4NDYzODUifQ' 1577 | } 1578 | }, 1579 | response: { 1580 | clearCookie: function(cookieName, options) { 1581 | clearCookieLogs.push({ 1582 | name: cookieName, 1583 | path: options.path, 1584 | domain: options.domain 1585 | }); 1586 | } 1587 | } 1588 | }); 1589 | 1590 | var clearCookieLogs = []; 1591 | 1592 | facebook.destroySession(); 1593 | 1594 | assert.equal(clearCookieLogs.length, 1); 1595 | assert.equal(clearCookieLogs[0].name, 'fbsr_117743971608120'); 1596 | assert.equal(clearCookieLogs[0].path, '/'); 1597 | assert.equal(clearCookieLogs[0].domain, '.www.test.com'); 1598 | 1599 | var facebook = new TransientFacebook({ 1600 | appId: '117743971608120', 1601 | secret: '943716006e74d9b9283d4d5d8ab93204', 1602 | request: { 1603 | headers: { 1604 | host: 'www.test.com' 1605 | }, 1606 | cookies: { 1607 | fbsr_117743971608120: '1sxR88U4SW9m6QnSxwCEw_CObqsllXhnpP5j2pxD97c.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsImV4cGlyZXMiOjEyODEwNTI4MDAsIm9hdXRoX3Rva2VuIjoiMTE3NzQzOTcxNjA4MTIwfDIuVlNUUWpub3hYVVNYd1RzcDB1U2g5d19fLjg2NDAwLjEyODEwNTI4MDAtMTY3Nzg0NjM4NXx4NURORHBtcy1nMUM0dUJHQVYzSVdRX2pYV0kuIiwidXNlcl9pZCI6IjE2Nzc4NDYzODUifQ', 1608 | fbm_117743971608120: 'base_domain=basedomain.test.com' 1609 | } 1610 | }, 1611 | response: { 1612 | clearCookie: function(cookieName, options) { 1613 | clearCookieLogs.push({ 1614 | name: cookieName, 1615 | path: options.path, 1616 | domain: options.domain 1617 | }); 1618 | } 1619 | } 1620 | }); 1621 | 1622 | clearCookieLogs = []; 1623 | 1624 | facebook.destroySession(); 1625 | 1626 | assert.equal(clearCookieLogs.length, 1); 1627 | assert.equal(clearCookieLogs[0].name, 'fbsr_117743971608120'); 1628 | assert.equal(clearCookieLogs[0].path, '/'); 1629 | assert.equal(clearCookieLogs[0].domain, 'basedomain.test.com'); 1630 | 1631 | done = true; 1632 | }, 1633 | 1634 | makeRequest: function(beforeExit, assert) { 1635 | var done = false; 1636 | beforeExit(function() { assert.ok(done) }); 1637 | var facebook = new TransientFacebook({ 1638 | appId: config.appId, 1639 | secret: config.secret 1640 | }); 1641 | 1642 | facebook.makeRequest('graph.facebook.com', '/amachang', { method: 'GET' }, function(err, data) { 1643 | assert.equal(err, null); 1644 | assert.notEqual(data, null); 1645 | assert.equal(JSON.parse(data).id, '1055572299'); 1646 | done = true; 1647 | }); 1648 | } 1649 | }; 1650 | 1651 | function TransientFacebook(params) { 1652 | this.store = this.mergeObject({}, params.store || {}); 1653 | BaseFacebook.apply(this, arguments); 1654 | }; 1655 | 1656 | util.inherits(TransientFacebook, BaseFacebook); 1657 | 1658 | TransientFacebook.prototype.setPersistentData = function(key, value) { 1659 | this.store[key] = value; 1660 | }; 1661 | 1662 | TransientFacebook.prototype.getPersistentData = function(key, defaultValue) { 1663 | return this.store.hasOwnProperty(key) ? (this.store[key]) : (defaultValue === undefined ? false : defaultValue); 1664 | }; 1665 | 1666 | TransientFacebook.prototype.clearPersistentData = function(key) { 1667 | delete this.store[key]; 1668 | }; 1669 | 1670 | TransientFacebook.prototype.clearAllPersistentData = function() { 1671 | this.store = {}; 1672 | }; 1673 | 1674 | function isArray(ar) { 1675 | return Array.isArray(ar) || (typeof ar === 'object' && Object.prototype.toString.call(ar) === '[object Array]'); 1676 | } 1677 | 1678 | -------------------------------------------------------------------------------- /test/cbutil.test.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var testUtil = require('./lib/testutil.js'); 3 | var cb = require(path.join(testUtil.libdir, 'cbutil.js')); 4 | 5 | module.exports = { 6 | 7 | callWrappedFunction: function(beforeExit, assert) { 8 | var done = false; 9 | beforeExit(function() { assert.ok(done) }); 10 | 11 | cb.wrap(setTimeout, true, 0)(function(err, d) { 12 | assert.equal(err, null); 13 | done = true; 14 | }, 10); 15 | }, 16 | 17 | callDoubleWrappedFunction: function(beforeExit, assert) { 18 | var done = false; 19 | beforeExit(function() { assert.ok(done) }); 20 | 21 | cb.wrap(cb.wrap(setTimeout, true, 0), true, 0)(function(err, d) { 22 | assert.equal(err, null); 23 | done = true; 24 | }, 10); 25 | }, 26 | 27 | throwsThenCallbackIsNotAFunction: function(beforeExit, assert) { 28 | var done = false; 29 | beforeExit(function() { assert.ok(done) }); 30 | 31 | assert.throws(function() { 32 | cb.wrap(setTimeout, true, 0)(null, 10); 33 | }, Error); 34 | 35 | done = true; 36 | }, 37 | 38 | throwsInWrappeeFunction: function(beforeExit, assert) { 39 | var done = false; 40 | beforeExit(function() { assert.ok(done) }); 41 | 42 | var wrapped = cb.wrap(function wrapped(callback) { 43 | throw Error('test'); 44 | }); 45 | 46 | wrapped(function callback(err, data) { 47 | assert.equal(data, null); 48 | assert.notEqual(err, null); 49 | assert.equal(err.message, 'test'); 50 | done = true; 51 | }); 52 | }, 53 | 54 | callbackIsCalledOnce: function(beforeExit, assert) { 55 | var calledCount = 0; 56 | var errorCount = 0; 57 | 58 | var write_ = process.stderr.write 59 | beforeExit(function() { 60 | process.stderr.write = write_; 61 | assert.equal(calledCount, 1); 62 | assert.equal(errorCount, 2); 63 | assert.ok(done) 64 | }); 65 | 66 | var log = cb.errorLog; 67 | cb.errorLog = function(msg) { 68 | errorCount++; 69 | }; 70 | 71 | var wrapped = cb.wrap(function wrapped(callback) { 72 | callback(null, 1); 73 | callback(null, 2); // error 1; 74 | }); 75 | 76 | wrapped(function callback(err, data) { 77 | calledCount++; 78 | done = true; 79 | throw Error('test'); // error 2 80 | }); 81 | 82 | cb.errorLog = log; 83 | } 84 | 85 | }; 86 | 87 | 88 | -------------------------------------------------------------------------------- /test/facebook.test.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | var testUtil = require('./lib/testutil.js'); 4 | 5 | var Facebook = require(path.join(testUtil.libdir, 'facebook.js')); 6 | 7 | var config = testUtil.fbDefaultConfig; 8 | 9 | module.exports = { 10 | 11 | clearAllPersistentData: function(beforeExit, assert) { 12 | var done = false; 13 | beforeExit(function() { assert.ok(done) }); 14 | 15 | var app = express.createServer(); 16 | app.configure(function () { 17 | app.use(express.bodyParser()); 18 | app.use(express.cookieParser()); 19 | app.use(express.session({ secret: 'foo bar' })); 20 | app.use(Facebook.middleware(config)); 21 | }); 22 | 23 | // Test clearAllPersistentData don't break session 24 | app.get('/', function(req, res) { 25 | req.facebook.clearAllPersistentData(); 26 | if (req.session.cookie) { 27 | res.send('ok'); 28 | } 29 | else { 30 | res.send('ng'); 31 | } 32 | }); 33 | 34 | assert.response(app, { url: '/' }, function(res) { 35 | assert.equal(res.body, 'ok'); 36 | done = true; 37 | }); 38 | }, 39 | 40 | getPersistentData: function(beforeExit, assert) { 41 | var done = false; 42 | beforeExit(function() { assert.ok(done) }); 43 | 44 | var app = express.createServer(); 45 | app.configure(function () { 46 | app.use(express.bodyParser()); 47 | app.use(express.cookieParser()); 48 | app.use(Facebook.middleware(config)); 49 | }); 50 | 51 | // When there is no session, getPersistentData return defaultValue 52 | app.get('/', function(req, res) { 53 | var user = req.facebook.getPersistentData('user_id', 0); 54 | res.send(JSON.stringify(user)); 55 | }); 56 | 57 | assert.response(app, { url: '/' }, function(res) { 58 | assert.equal(res.body, '0'); 59 | done = true; 60 | }); 61 | }, 62 | 63 | middleware: function(beforeExit, assert) { 64 | var done = false; 65 | beforeExit(function() { assert.ok(done) }); 66 | 67 | var app = express.createServer(); 68 | app.configure(function () { 69 | app.use(express.bodyParser()); 70 | app.use(express.cookieParser()); 71 | app.use(express.session({ secret: 'foo bar' })); 72 | app.use(Facebook.middleware(config)); 73 | }); 74 | 75 | app.get('/', function(req, res) { 76 | if (req.facebook) { 77 | res.send('ok'); 78 | } 79 | else { 80 | res.send('ng'); 81 | } 82 | }); 83 | 84 | assert.response(app, { url: '/' }, function(res) { 85 | assert.equal(res.body, 'ok'); 86 | done = true; 87 | }); 88 | }, 89 | 90 | loginRequired: function(beforeExit, assert) { 91 | var done = false; 92 | beforeExit(function() { assert.ok(done) }); 93 | 94 | var app = express.createServer(); 95 | app.configure(function () { 96 | app.use(express.bodyParser()); 97 | app.use(express.cookieParser()); 98 | app.use(express.session({ secret: 'foo bar' })); 99 | app.use(Facebook.middleware(config)); 100 | }); 101 | 102 | app.get('/', Facebook.loginRequired(), function(req, res) { 103 | req.facebook.getUser(function(err, user) { 104 | res.send(user); 105 | }); 106 | }); 107 | 108 | assert.response(app, { url: '/' }, function(res) { 109 | assert.equal(res.statusCode, 302); 110 | 111 | done = true; 112 | }); 113 | } 114 | }; 115 | 116 | 117 | -------------------------------------------------------------------------------- /test/lib/testutil.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | 4 | exports.basedir = path.join(__dirname, '..', '..'); 5 | var covdir = path.join(exports.basedir, 'lib-cov'); 6 | var libdir = path.join(exports.basedir, 'lib'); 7 | 8 | try { 9 | var stat = fs.statSync(covdir); 10 | if (stat.isDirectory()) { 11 | libdir = covdir; 12 | } 13 | } 14 | catch (e) { 15 | } 16 | 17 | exports.libdir = libdir; 18 | 19 | exports.fbDefaultConfig = { 20 | appId: '227710073967374', 21 | secret: 'a25a2216fb1b772f1c554ebb9d950aec' 22 | } 23 | 24 | if ('TEST_FB_APP_ID' in process.env) { 25 | exports.fbDefaultConfig.appId = process.env.TEST_FB_APP_ID; 26 | } 27 | if ('TEST_FB_SECRET' in process.env) { 28 | exports.fbDefaultConfig.secret = process.env.TEST_FB_SECRET; 29 | } 30 | 31 | -------------------------------------------------------------------------------- /test/multipart.test.js: -------------------------------------------------------------------------------- 1 | 2 | var path = require('path'); 3 | var util = require('util'); 4 | var fs = require('fs'); 5 | var stream = require('stream'); 6 | 7 | var basedir = path.join(__dirname, '..'); 8 | var covdir = path.join(basedir, 'lib-cov'); 9 | var libdir = path.join(basedir, 'lib'); 10 | 11 | try { 12 | var stat = fs.statSync(covdir); 13 | if (stat.isDirectory()) { 14 | libdir = covdir; 15 | } 16 | } 17 | catch (e) { 18 | } 19 | 20 | var Multipart = require(path.join(libdir, 'multipart.js')); 21 | 22 | module.exports = { 23 | 24 | constructor: function(beforeExit, assert) { 25 | var done = false; 26 | beforeExit(function() { assert.ok(done) }); 27 | 28 | var multipart = new Multipart(); 29 | assert.equal(multipart.dash.length, 2); 30 | assert.equal(multipart.dash.toString('ascii'), '--'); 31 | assert.ok(multipart.boundary.length > 0); 32 | assert.ok(multipart.boundary.toString('ascii').match(/^[0-9a-z]+$/)); 33 | 34 | done = true; 35 | }, 36 | 37 | contentLength: function(beforeExit, assert) { 38 | var done = false; 39 | beforeExit(function() { assert.ok(done) }); 40 | 41 | var multipart = new Multipart(); 42 | multipart.addText('foo', 'bar'); 43 | multipart.addFile('src', __filename, function(err) { 44 | assert.equal(err, null); 45 | 46 | function CounterStream() { 47 | this.writable = true; 48 | this.length = 0; 49 | } 50 | util.inherits(CounterStream, stream.Stream); 51 | CounterStream.prototype.write = function(data) { 52 | this.length += data.length 53 | return true; 54 | }; 55 | CounterStream.prototype.end = function(data) { 56 | if (data) this.write(data); 57 | this.writable = false; 58 | return true; 59 | } 60 | CounterStream.prototype.destroy = function() {}; 61 | CounterStream.prototype.destroySoon = function() {}; 62 | 63 | var contentLength = multipart.getContentLength(); 64 | 65 | var counter = new CounterStream(); 66 | multipart.writeToStream(counter, function() { 67 | assert.equal(counter.length, contentLength); 68 | 69 | done = true; 70 | }); 71 | }); 72 | } 73 | 74 | }; 75 | 76 | -------------------------------------------------------------------------------- /test/requestutil.test.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var http = require('http'); 3 | var https = require('https'); 4 | var testUtil = require('./lib/testutil.js'); 5 | var requestUtil = require(path.join(testUtil.libdir, 'requestutil.js')); 6 | 7 | module.exports = { 8 | simpleRequest: function(beforeExit, assert) { 9 | var e = null; 10 | var d = null; 11 | var done = false; 12 | beforeExit(function() { 13 | assert.equal(e, null); 14 | assert.notEqual(d, null); 15 | assert.equal(JSON.parse(d).id, '1055572299'); 16 | assert.ok(done); 17 | }); 18 | 19 | var req = new requestUtil.requestFacebookApi(https, 'graph.facebook.com', 443, '/amachang', { method: 'GET' }, false, function(err, data) { 20 | e = err; 21 | d = data; 22 | done = true; 23 | }); 24 | }, 25 | 26 | simpleMultipartRequest: function(beforeExit, assert) { 27 | var e = null; 28 | var d = null; 29 | var done = false; 30 | beforeExit(function() { 31 | assert.equal(e, null); 32 | assert.notEqual(d, null); 33 | assert.equal(JSON.parse(d).id, '1055572299'); 34 | assert.ok(done); 35 | }); 36 | 37 | var req = new requestUtil.requestFacebookApi(https, 'graph.facebook.com', 443, '/amachang', { method: 'GET' }, true, function(err, data) { 38 | e = err; 39 | d = data; 40 | done = true; 41 | }); 42 | }, 43 | 44 | constructorAndStart: function(beforeExit, assert) { 45 | var e = null; 46 | var d = null; 47 | var done = false; 48 | beforeExit(function() { 49 | assert.equal(e, null); 50 | assert.notEqual(d, null); 51 | assert.equal(JSON.parse(d).id, '1055572299'); 52 | assert.ok(done); 53 | }); 54 | 55 | var req = new requestUtil.FacebookApiRequest(https, 'graph.facebook.com', 443, '/amachang', { method: 'GET' }); 56 | req.start(false, function(err, data) { 57 | e = err; 58 | d = data; 59 | done = true; 60 | }); 61 | }, 62 | 63 | responseError: function(beforeExit, assert) { 64 | var e = null; 65 | var d = null; 66 | var done = false; 67 | beforeExit(function() { 68 | assert.notEqual(e, null); 69 | assert.equal(d, null); 70 | assert.equal(e.code, 'ENOTFOUND'); 71 | assert.ok(done); 72 | }); 73 | 74 | var req = new requestUtil.FacebookApiRequest(http, 'notfound.example.com', 80, '/', { method: 'GET' }); 75 | 76 | req.start(false, function(err, data) { 77 | e = err; 78 | d = data; 79 | done = true; 80 | }); 81 | }, 82 | 83 | throwErrorAfterResponse: function(beforeExit, assert) { 84 | var e = null; 85 | var d = null; 86 | var done = false; 87 | beforeExit(function() { 88 | assert.notEqual(e, null); 89 | assert.equal(d, null); 90 | assert.equal(e.message, 'addListener only takes instances of Function'); 91 | assert.ok(done); 92 | }); 93 | 94 | var req = new requestUtil.FacebookApiRequest(https, 'graph.facebook.com', 443, '/amachang', { method: 'GET' }); 95 | 96 | // break process 97 | req.selfBoundDataHandler = null; 98 | req.start(false, function(err, data) { 99 | e = err; 100 | d = data; 101 | done = true; 102 | }); 103 | }, 104 | 105 | dataError: function(beforeExit, assert) { 106 | var e = null; 107 | var d = null; 108 | var done = false; 109 | beforeExit(function() { 110 | assert.notEqual(e, null); 111 | assert.equal(d, null); 112 | assert.equal(e.message, 'dummy'); 113 | assert.ok(done); 114 | }); 115 | 116 | var req = new requestUtil.FacebookApiRequest(https, 'graph.facebook.com', 443, '/amachang', { method: 'GET' }); 117 | 118 | req.afterResponse_ = req.afterResponse; 119 | req.afterResponse = function() { 120 | process.nextTick(function() { 121 | req.handleDataError(new Error('dummy')); 122 | }); 123 | return req.afterResponse_.apply(this, arguments); 124 | }; 125 | 126 | req.start(false, function(err, data) { 127 | e = err; 128 | d = data; 129 | done = true; 130 | }); 131 | }, 132 | 133 | throwErrorInEndData: function(beforeExit, assert) { 134 | var e = null; 135 | var d = null; 136 | var done = false; 137 | beforeExit(function() { 138 | assert.notEqual(e, null); 139 | assert.equal(d, null); 140 | assert.ok(done); 141 | }); 142 | 143 | var req = new requestUtil.FacebookApiRequest(https, 'graph.facebook.com', 443, '/amachang', { method: 'GET' }); 144 | 145 | req.detachDataAndEndAndErrorHandlers = null; 146 | req.start(false, function(err, data) { 147 | e = err; 148 | d = data; 149 | done = true; 150 | }); 151 | } 152 | }; 153 | -------------------------------------------------------------------------------- /util/clean.bash: -------------------------------------------------------------------------------- 1 | rm -rf lib-cov 2 | 3 | -------------------------------------------------------------------------------- /util/create_signed_request.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var crypto = require('crypto'); 3 | var assert = require('assert'); 4 | 5 | assert.equal(process.argv.length, 3, 'Command line arguments length must be 3'); 6 | assert.ok('TEST_FB_SECRET' in process.env); 7 | 8 | console.log(createSignedRequest(process.argv[2], process.env.TEST_FB_SECRET)); 9 | 10 | function createSignedRequest(json, secret) { 11 | var payload = encodeBase64Url(json); 12 | 13 | var hmac = crypto.createHmac('sha256', secret); 14 | hmac.update(payload); 15 | var encodedSig = hmac.digest('base64'); 16 | encodedSig = base64ToBase64Url(encodedSig); 17 | 18 | return encodedSig + '.' + payload; 19 | } 20 | 21 | function encodeBase64Url(str) { 22 | var buffer = new Buffer(str, 'utf8'); 23 | var base64 = buffer.toString('base64'); 24 | var base64url = base64ToBase64Url(base64); 25 | return base64url; 26 | } 27 | 28 | function base64ToBase64Url(base64) { 29 | var base64url = base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); 30 | return base64url; 31 | } 32 | 33 | --------------------------------------------------------------------------------