1 /** 2 * @fileOverview 3 * 4 * Library for building facebook apps. 5 * <br /> 6 * Importing facebook will automatically make all known FBML tags available 7 * as functional HTML tags, with : replaced with _. For example, the 8 * you can create a FB:FRIEND-SELECTOR tag by calling FB_FRIEND_SELECTOR(). 9 * 10 * See: http://wiki.developers.facebook.com/index.php/Category:FBML_tags 11 */ 12 13 //---------------------------------------------------------------- 14 // dependencies 15 //---------------------------------------------------------------- 16 importLocal(this, "quickforms", "storage"); 17 18 //---------------------------------------------------------------- 19 // FBML tags 20 //---------------------------------------------------------------- 21 var _fbmltags = 22 [ 23 "FBML", 24 "FB:18-PLUS", 25 "FB:21-PLUS", 26 "FB:CAPTCHA", 27 "FB:ACTION", 28 "FB:ADD-SECTION-BUTTON", 29 "FB:APPLICATION-NAME", 30 "FB:ATTACHMENT-PREVIEW", 31 "FB:BOARD", 32 "FB:COMMENTS", 33 "FB:CREATE-BUTTON", 34 "FB:DASHBOARD", 35 "FB:DEFAULT", 36 "FB:DIALOG", 37 "FB:DIALOG-BUTTON", 38 "FB:DIALOG-CONTENT", 39 "FB:DIALOG-DISPLAY", 40 "FB:DIALOG-TITLE", 41 "FB:EDITOR", 42 "FB:EDITOR-BUTTON", 43 "FB:EDITOR-BUTTONSET", 44 "FB:EDITOR-CANCEL", 45 "FB:EDITOR-CUSTOM", 46 "FB:EDITOR-DATE", 47 "FB:EDITOR-DIVIDER", 48 "FB:EDITOR-MONTH", 49 "FB:EDITOR-TEXT", 50 "FB:EDITOR-TEXTAREA", 51 "FB:EDITOR-TIME", 52 "FB:ELSE", 53 "FB:ERROR", 54 "FB:EVENTLINK", 55 "FB:EXPLANATION", 56 "FB:FBJS_BRIDGE", 57 "FB:FBML", 58 "FB:FBMLVERSION", 59 "FB:FLV", 60 "FB:FRIEND-SELECTOR", 61 "FB:GOOGLE-ANALYTICS", 62 "FB:GROUPLINK", 63 "FB:HEADER", 64 "FB:HEADER-TITLE", 65 "FB:HELP", 66 "FB:IF", 67 "FB:IF-CAN-SEE", 68 "FB:IF-CAN-SEE-EVENT", 69 "FB:IF-CAN-SEE-PHOTO", 70 "FB:IF-IS-APP-USER", 71 "FB:IF-IS-FRIENDS-WITH-VIEWER", 72 "FB:IF-IS-GROUP-MEMBER", 73 "FB:IF-IS-OWN-PROFILE", 74 "FB:IF-IS-USER", 75 "FB:IF-MULTIPLE-ACTORS", 76 "FB:IF-USER-HAS-ADDED-APP", 77 "FB:IFRAME", 78 "FB:IS-IN-NETWORK", 79 "FB:IS-IT-APRIL-FOOLS", 80 "FB:IS-LOGGED-OUT", 81 "FB:JS-STRING", 82 "FB:MEDIAHEADER", 83 "FB:MESSAGE", 84 "FB:MP3", 85 "FB:MULTI-FRIEND-INPUT", 86 "FB:MULTI-FRIEND-SELECTOR", 87 "FB:NAME", 88 "FB:NARROW", 89 "FB:NETWORKLINK", 90 "FB:NOTIF-EMAIL", 91 "FB:NOTIF-PAGE", 92 "FB:NOTIF-SUBJECT", 93 "FB:OWNER-ACTION", 94 "FB:PHOTO", 95 "FB:PROFILE-ACTION", 96 "FB:PROFILE-PIC", 97 "FB:PRONOUN", 98 "FB:RANDOM", 99 "FB:RANDOM-OPTION", 100 "FB:REDIRECT", 101 "FB:REF", 102 "FB:REQ-CHOICE", 103 "FB:REQUEST-FORM", 104 "FB:REQUEST-FORM-SUBMIT", 105 "FB:SHARE-BUTTON", 106 "FB:SILVERLIGHT", 107 "FB:SUBMIT", 108 "FB:SUBTITLE", 109 "FB:SUCCESS", 110 "FB:SWF", 111 "FB:SWITCH", 112 "FB:TAB-ITEM", 113 "FB:TABS", 114 "FB:TIME", 115 "FB:TITLE", 116 "FB:TYPEAHEAD-INPUT", 117 "FB:TYPEAHEAD-OPTION", 118 "FB:USER", 119 "FB:USER-ITEM", 120 "FB:USER-STATUS", 121 "FB:USER-TABLE", 122 "FB:USERLINK", 123 "FB:VISIBLE-TO-ADDED-APP-USERS", 124 "FB:VISIBLE-TO-APP-USERS", 125 "FB:VISIBLE-TO-CONNECTION", 126 "FB:VISIBLE-TO-FRIENDS", 127 "FB:VISIBLE-TO-OWNER", 128 "FB:VISIBLE-TO-USER", 129 "FB:WALLPOST", 130 "FB:WALLPOST-ACTION", 131 "FB:WIDE" 132 ]; 133 importTags(this, _fbmltags); 134 135 //---------------------------------------------------------------- 136 // persistent things 137 //---------------------------------------------------------------- 138 function _getstorage(key) { 139 if (storage['facebooklib'] === undefined) { 140 return undefined; 141 } 142 return storage.facebooklib[key]; 143 } 144 145 function _setstorage(key, val) { 146 if (storage.facebooklib === undefined) { 147 storage.facebooklib = new StorableObject(); 148 } 149 storage.facebooklib[key] = val; 150 } 151 152 //---------------------------------------------------------------- 153 // global fb singleton object 154 //---------------------------------------------------------------- 155 156 /** 157 * The FaceBook object. 158 * @type {object} 159 */ 160 fb = {}; 161 162 /** 163 * The facebook uid of the current user of this app. 164 * @type {string} 165 */ 166 fb.uid = '-1'; 167 168 /** 169 * The current facebook sessionKey. This will only be set if the session user has added the 170 * app and is logged in to it. Otherwise, it will be undefined. 171 * @type {string} 172 */ 173 fb.sessionKey = undefined; 174 if (request.param('fb_sig_session_key')) { 175 fb.sessionKey = request.param('fb_sig_session_key'); 176 } 177 178 /** 179 * List of the uids of the current user's friends. This will only be filled in 180 * if the session user has added the app and is logged in to it. 181 * @type {array[string]} 182 */ 183 fb.friends = []; 184 if (request.param('fb_sig_friends')) { 185 fb.friends = request.param('fb_sig_friends').split(','); 186 } 187 188 // module private vars 189 var _isSetup = true; 190 191 /** 192 * @ignore 193 */ 194 var _config = {} 195 _config.canvasUrl = null; 196 _config.secret = null; 197 _config.apiKey = null; 198 199 200 //---------------------------------------------------------------- 201 // fill in some default values 202 //---------------------------------------------------------------- 203 if (request.param('fb_sig_user')) { 204 fb.uid = request.param('fb_sig_user'); 205 } 206 207 ['secret', 'apiKey', 'canvasUrl'].forEach(function(k) { 208 if (_getstorage(k)) { 209 _config[k] = _getstorage(k); 210 } else { 211 _isSetup = false; 212 } 213 }); 214 215 /** 216 * contains "http://apps.facebook.com/<the_canvasl_url>/" 217 * @type {string} 218 */ 219 fb.fullCanvasUrl = ""; 220 221 if (_config.canvasUrl) { 222 fb.fullCanvasUrl = 'http://apps.facebook.com/' + _config.canvasUrl + '/'; 223 } 224 225 //---------------------------------------------------------------- 226 // Utilities 227 //---------------------------------------------------------------- 228 229 // calculates the hash (md5 digest) of a map, according 230 // to Facebook's auth rules 231 function _objectHash(obj) { 232 var kk = keys(obj); 233 kk.sort(); 234 var sigstring = ""; 235 kk.forEach(function(k) { sigstring += (k + "=" + obj[k]); }); 236 sigstring += _config.secret; 237 return md5(sigstring); 238 } 239 240 /** 241 * Tells facebook to redirect the user to a new URL. For FBML facebook apps, use 242 * this instead of response.redirect(), so that it redirects the facebook user 243 * instead of the facebook server. 244 * @param {string} [url] The new URL to direct the user to. If you do not set this parameter, 245 * the user will be redirected to the main canvas page. 246 */ 247 fb.redirect = function(url) { 248 if (!url) { 249 url = fb.fullCanvasUrl; 250 } 251 if (request.param('fb_sig_in_canvas') == "1") { 252 print(FB_REDIRECT({url: url})); 253 response.stop(true); 254 return; 255 } 256 if (url.match(/^https?:\/\/([^\/]*\.)?facebook\.com(:\d+)?/i)) { 257 // make sure facebook.com url's load in the full frame so that we don't 258 // get a frame within a frame. 259 print(raw('<script type="text/javascript">\ntop.location.href = "' 260 +url+'";\n</script>')); 261 response.stop(true); 262 return; 263 } 264 response.redirect(url); 265 }; 266 267 /** 268 * Ensures that the current user has added this app. If the current user of the app 269 * has already added it, this does nothing. Otherwise, it immediately redirects the 270 * user to the add page. 271 */ 272 fb.requireAdd = function() { 273 if (request.param('fb_sig_added') != '1') { 274 fb.redirect("http://www.facebook.com/add.php?api_key="+_config.apiKey); 275 } 276 }; 277 278 //---------------------------------------------------------------- 279 // init() 280 //---------------------------------------------------------------- 281 282 /** 283 * Every facebook app should call fb.init() immediately after import("facebook"). 284 * It will first make sure the AppJet app is configured as a valid facebook app. Next, if 285 * the request path starts with "/callback/", it will 286 * authenticate that the request from facebook and then set up all the fb.* properties. 287 */ 288 fb.init = function() { 289 _checkSetup(); // will stop request if preview or not set up 290 291 if (request.isGET && request.path == '/') { 292 // web request -- not a facebook-proxied request 293 print(P("This is a facebook application. Visit it at: ", 294 A({href: fb.fullCanvasUrl}, fb.fullCanvasUrl))); 295 response.stop(true); 296 } 297 298 page.setMode('facebook'); 299 300 if (request.isPOST && /^\/callback\//.test(request.path)) { 301 if (!fb.authenticateRequest()) { 302 print(FB_DASHBOARD(), 303 FB_ERROR(FB_MESSAGE("Authentication Error: Forbidden"), 304 "We could not verify that this request came from Facebook. ", 305 "For your safety and privacy, we will not display the page. ", 306 BR(), 307 "Please report this to the creators of the application.")); 308 response.setStatusCode(403); 309 response.stop(true); 310 } 311 } 312 }; 313 314 //---------------------------------------------------------------- 315 // Facebook REST functions 316 //---------------------------------------------------------------- 317 318 // TODO: catch error codes and return them somehow and/or throw exceptions with useful messages. 319 320 /** 321 * Executes a remote facebook API call. See the <a 322 * href="http://wiki.developers.facebook.com/index.php/API">Facebok API 323 * Methods</a> wiki page for a list of all methods. 324 * 325 * @param {string} cmd The name of the command, e.g. "facebook.profile.setFBML". 326 * @param {object} params The set of parameters to pass to this call. Note: api_key, sesion_key, 327 * call_id, v, and format, and sig will be automatically set for you. However, you 328 * can override any of these if you like. 329 * @return {object} The call result, as a javascript object. 330 * 331 * @example 332 fb.requireAdd(); 333 print(fb.call("users.getInfo", 334 { uids: [700577, 550505927], 335 fields: ["birthday", "about_me"] })); 336 * 337 */ 338 fb.call = function(cmd, params) { 339 if ((! fb.sessionKey) && (!params.session_key)) { 340 throw new Error("No session key for fb.call(); to make calls you should" 341 +" require that the user be logged in or have added your" 342 +" app, OR you can use your own infinite session key" 343 +" for the call."); 344 } 345 function formatParam(p) { 346 if (p instanceof Array) { 347 return p.join(','); 348 } else { 349 return p.toString(); 350 } 351 } 352 var params2 = {}; 353 params2.method = cmd; 354 params2.api_key = _config.apiKey; 355 params2.session_key = fb.sessionKey; 356 params2.call_id = (+(new Date())); 357 params2.v = "1.0"; 358 params2.format = "JSON"; 359 eachProperty(params, function(k, v) { 360 params2[k] = formatParam(v); 361 }); 362 params2.sig = _objectHash(params2); 363 364 postResult = wpost("http://api.facebook.com/restserver.php", params2); 365 if (postResult.substr(0, 6).toLowerCase() == '<?xml ') { 366 print("Facebook API Error: ", PRE(postResult)); 367 return null; 368 } 369 // decided to use "eval" parse the JSON for now, since 370 // it's coming from facebook and can't do harm if it follows the spec 371 var result = eval("("+postResult+")"); 372 if (result.error_msg && result.error_code) { 373 print("Facebook API Error: "); 374 print(result); 375 } 376 return result; 377 }; 378 379 /** 380 * Convenience function for sending a notification to 1 or more users. 381 * 382 * See: <a href="http://developers.facebook.com/documentation.php?v=1.0&method=notifications.send"> 383 * Facebook's docs for facebook.notifications.send</a>. 384 * 385 * @param {array or string} users Array of uids or single uid of those to receive this notification. 386 * @param {string} notificationFBML The FBML content of the notification. 387 */ 388 fb.sendNotification = function(users, notificationFBML) { 389 var userStr = users; 390 if (Array.prototype.isPrototypeOf(users)) { 391 userStr = users.join(','); 392 } 393 fb.call("facebook.notifications.send", 394 { to_ids: userStr, notification: notificationFBML }); 395 }; 396 397 /** 398 * Convenience function for setting a user's profile FBML. 399 * 400 * See <a 401 * href="http://developers.facebook.com/documentation.php?v=1.0&method=profile.setFBML">Facebook's 402 * docs for facebook.profile.setFBML</a>. 403 * 404 * @param {string} uid The uid of the profile to set. 405 * @param {string} markup The text of the FBML to set. 406 * @example 407 fb.requireAdd(); 408 fb.setProfileFBML(fb.uid, 409 DIV(CENTER( 410 "Here is another picture of me:", 411 BR(), 412 FB_PROFILE_PIC({uid: fb.uid})))); 413 */ 414 fb.setProfileFBML = function(uid, markup) { 415 if (markup.toHTML && typeof(markup.toHTML) == "function") { 416 markup = markup.toHTML(); 417 } 418 fb.call("facebook.profile.setFBML", 419 { uid: uid, markup: markup }); 420 }; 421 422 //---------------------------------------------------------------- 423 // Facebook Request Authentication 424 //---------------------------------------------------------------- 425 426 /** 427 * Checks post parameters against fb_sig to verify that the reqest 428 * came from facebook servers and not a haxxor. Note: this method is called automatically by 429 * fb.init(), so you usually do not need to call it directly. 430 * 431 * @return {boolean} true if the request is from facebook and legit; false otherwise. 432 */ 433 fb.authenticateRequest = function() { 434 var fbParams = {}; 435 for(p in request.params) { 436 if (p.match("^fb_sig_")) { 437 fbParams[p.substring("fb_sig_".length)] = request.params[p]; 438 } 439 } 440 var fbCorrectSig = _objectHash(fbParams); 441 return (fbCorrectSig == request.param("fb_sig")); 442 }; 443 444 //---------------------------------------------------------------- 445 // config setup 446 //---------------------------------------------------------------- 447 448 // stops the request if this is a preview or the app is not setup 449 function _checkSetup() { 450 451 var isSetup = (_isSetup === true); 452 453 // security 454 if (!appjet.isPreview) { 455 if (! isSetup) { 456 print(P("This is an unconfigured facebook app. If you are the app developer,", 457 " preview this app in the AppJet IDE to configure it.")); 458 response.stop(true); 459 } else { 460 return; // not preview mode AND app is setup... proceed to render the app 461 } 462 } 463 464 page.head.write(STYLE(raw(""" 465 #container { height: 100%; position: relative; overflow: auto; } 466 iframe#newapp { width: 800px; border: 0; height: 2000px; position: absolute; 467 left: 150px; top: 0px; overflow: hidden; z-index: 1 } 468 #appjetfooter { display: none; } 469 #instrucs { position: absolute; left: 0; top: 0; width: 300px; z-index: 2; background: #ddd; 470 height: 2000px; } 471 #instrucsinner { padding: 0 1em; margin-top: 1em; } 472 #instrucs p { margin: 1em 0; } 473 #instrucs h2 { font-size: 110%; margin: 0; line-height: 120%; 474 text-align: center; padding: 0.5em; background: #666; 475 padding-top: 0.8em; color: #fff; } 476 .notpublished { color: #a00; } 477 .instrucPanel { display: none; } 478 #needLoginPanel { display: block; } 479 .buttons .prev { float: left; } 480 .buttons .next { float: right; } 481 .url { font-size: 90%; } 482 .forcopy { border: 1px solid #999; padding: 5px; } 483 hr { clear: both; margin-top: 0.8em; margin-bottom: 0.8em; } 484 #configurePanel .field { font-size: 90%; } 485 li { margin: 0.5em; } 486 body { line-height: 120%; } 487 .small { font-size: 80%; } 488 """))); 489 490 if (request.path == "/wizard") { 491 /**** begin wizard ****/ 492 493 var myAppsPath = "http://www.facebook.com/developers/apps.php"; 494 var newAppPath = "http://www.facebook.com/developers/editapp.php?new"; 495 496 page.head.write(STYLE(raw(""" 497 html, body { height: 100%; overflow: hidden; } 498 body { margin: 0; padding: 0; } 499 """))); 500 501 function prevNext(prevPanel, nextPanel, opts) { 502 var prevButton = INPUT({className:"prev", type:"button", value:"<< Prev"}); 503 if (prevPanel) { 504 prevButton.attribs.onClick = "selectPanel('"+prevPanel+"');"; 505 } 506 else { 507 prevButton.attribs.disabled = "disabled"; 508 } 509 var nextButton = INPUT({className:"next", type:"button", value:"Next >>"}); 510 if (nextPanel) { 511 nextButton.attribs.onClick = "selectPanel('"+nextPanel+"');"; 512 } 513 else { 514 nextButton.attribs.disabled = "disabled"; 515 } 516 if (opts && opts.disablePrev) prevButton.attribs.disabled = "disabled"; 517 if (opts && opts.disableNext) nextButton.attribs.disabled = "disabled"; 518 if (opts && opts.prevJS && prevButton.attribs.onClick) 519 prevButton.attribs.onClick = prevButton.attribs.onClick+" "+opts.prevJS; 520 if (opts && opts.nextJS && nextButton.attribs.onClick) 521 nextButton.attribs.onClick = nextButton.attribs.onClick+" "+opts.nextJS; 522 if (opts && opts.noNext) nextButton = DIV(); 523 else if (opts && opts.replaceNextWith) nextButton = opts.replaceNextWith; 524 if (opts && opts.noPrev) prevButton = DIV(); 525 else if (opts && opts.replacePrevWith) prevButton = opts.replacePrevWith; 526 527 return DIV({className:"buttons"}, prevButton, nextButton); 528 } 529 530 function needLoginPanel() { 531 return DIV({className:"instrucPanel selectedPanel",id:"needLoginPanel"},P(raw(""" 532 If you're not already logged into Facebook, you'll be prompted here. (Your password 533 is not sent to AppJet.)""")), 534 P("Facebook may also ask permission to add its Developer interface to your account."), 535 P("When you see ",STRONG("My Applications")," click Next."), 536 prevNext(null,"creationPanel")); 537 } 538 539 function creationPanel() { 540 return DIV({className:"instrucPanel",id:"creationPanel"}, 541 P({id:"createappcont"}, 542 STRONG("Click me: "),INPUT({id:"createapp", type:"button", value:"Start New Facebook App", 543 onClick:"loadInIframe('"+newAppPath+"'); selectPanel('appNamePanel');"})), 544 P("Or, click Edit Settings on an app to the right."), 545 prevNext("needLoginPanel","appNamePanel")); 546 } 547 548 function appNamePanel() { 549 return DIV({className:"instrucPanel",id:"appNamePanel"}, 550 P(STRONG("Application Name:")," Choose a name for your app on Facebook."), 551 P("Remember to mark the checkbox if necessary."), 552 P("Expand ",STRONG("Optional Fields")," so that the triangle points down."), 553 prevNext("creationPanel","baseOptsPanel")); 554 } 555 556 function getCallbackURL() { 557 return "http://"+appjet.appName+"."+appjet.mainDomain+"/callback/"; 558 } 559 560 function baseOptsPanel() { 561 return DIV({className:"instrucPanel",id:"baseOptsPanel"}, 562 P(STRONG("Callback URL:")," Enter this:\n",SPAN({className:"url forcopy"}, 563 getCallbackURL())), 564 P(STRONG("Canvas Page URL:")," Choose one, enter it to the right, and tell me here:\n", 565 SPAN({className:"url"},"http://apps.facebook.com/", 566 INPUT({type:"text",id:"canvasPage",size:10,onkeypress: 567 "typeCanvasPageNotify();",onclick:"typeCanvasPageNotify();"}),"/")), 568 prevNext("appNamePanel","installOptsPanel",{disableNext:true, 569 nextJS:"typeCanvasPageNotify();"})); 570 } 571 572 function installOptsPanel() { 573 return DIV({className:"instrucPanel",id:"installOptsPanel"}, 574 P(STRONG("Can your application be added?"),' Click "Yes".'), 575 P("(Scroll down...)"), 576 P(STRONG("Who can add your application?"),' Mark "Users".'), 577 P(STRONG("Post-Add URL:"),"\n", 578 SPAN({className:"url forcopy"},"http://apps.facebook.com/", 579 SPAN({className:"canvasPageCopied"},"<canvasPage>"),"/")), 580 P(STRONG("Side Nav URL:"),"\n", 581 SPAN({className:"url forcopy"},"http://apps.facebook.com/", 582 SPAN({className:"canvasPageCopied"},"<canvasPage>"),"/")), 583 prevNext("baseOptsPanel","submitPanel")); 584 } 585 586 function submitPanel() { 587 return DIV({className:"instrucPanel",id:"submitPanel"}, 588 P("Those are the important settings."), 589 P(STRONG("When you're ready, click the blue Submit button.")), 590 P("I'll need some information from the next page..."), 591 prevNext("installOptsPanel","configurePanel")); 592 } 593 594 function configurePanel() { 595 return DIV({className:"instrucPanel",id:"configurePanel"}, 596 FORM({action:"/config",method:"post"}, 597 A({href:"javascript:void(loadInIframe('"+myAppsPath+"#content'));"}, "Scroll to top of My Applications"), 598 HR(), 599 P("Find the listing for your app and copy the following: "), 600 P({className:"field"},STRONG("API Key:"),"\n", 601 INPUT({type:"text",value:"",size:"32",name:"apikey"})), 602 P({className:"field"},STRONG("Secret:"),"\n", 603 INPUT({type:"text",value:"",size:"32",name:"secret"})), 604 HR(), 605 P("Confirm the following:"), 606 P({className:"field"},STRONG("Callback URL:"),"\n", 607 getCallbackURL()), 608 P({className:"field"},STRONG("Canvas Page (from earlier):"),"\n", 609 "http://apps.facebook.com/",INPUT({id:"canvasPageConfig", type:"text", 610 value:"XXXX", name:"canvaspage", size:10}),"/"), 611 HR(), 612 prevNext("submitPanel",null,{replaceNextWith: 613 INPUT({className:"next",type:"submit",value:"Done!"})}))); 614 } 615 616 page.setTitle("Facebook App Wizard"); 617 page.head.write(SCRIPT( 618 {src: 619 "http://cachefile.net/scripts/jquery/1.2.1/jquery-1.2.1.js"})); 620 print(DIV({id:"container"}, 621 IFRAME({id:"newapp",name:"newapp", 622 src:myAppsPath})), 623 DIV({id:"instrucs"},H2("Facebook App Configuration"), 624 DIV({id:"instrucsinner"}, 625 needLoginPanel(), creationPanel(), 626 appNamePanel(), 627 baseOptsPanel(), installOptsPanel(), 628 submitPanel(), configurePanel()))); 629 630 page.head.write(SCRIPT(raw(""" 631 function selectPanel(theId) { 632 $(".instrucPanel").hide().removeClass("selectedPanel"); 633 $("#"+theId).show().addClass("selectedPanel"); 634 } 635 636 function getCanvasPage() { 637 var cp = $("input#canvasPage").get(0).value; 638 cp = cp.replace(/^\s+/,'').replace(/\s+$/,''); 639 return cp; 640 } 641 642 function typeCanvasPageNotify() { 643 $('#baseOptsPanel .next').get(0).disabled = false; 644 var canvasPage = getCanvasPage(); 645 $(".canvasPageCopied").html(canvasPage); 646 $("#canvasPageConfig").get(0).value = canvasPage; 647 } 648 649 function loadInIframe(url) { 650 $('iframe#newapp').get(0).contentWindow.location.href = url; 651 } 652 """))); 653 /**** end wizard ****/ 654 } 655 else if (request.path == "/config" && request.isPost) { 656 _setstorage('apiKey', trim(request.params.apikey)); 657 _setstorage('secret', trim(request.params.secret)); 658 _setstorage('canvasUrl', trim(request.params.canvaspage)); 659 print(SCRIPT(raw("window.close();"))); 660 } 661 else { 662 // path "/", etc. 663 if (! isSetup) { 664 print(H2("Facebook Library: Unconfigured App")); 665 if (! appjet._native.hasBeenPublished()) { 666 print(P(SPAN({className:"notpublished"},'First, you have to give this app a public URL.'))); 667 print(UL(LI