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  * @constructor
160  * @class Library for building facebook apps. Importing facebook will automatically make all known FBML tags available as functional HTML tags, with : replaced with _. For example, the you can create a FB:FRIEND-SELECTOR tag by calling FB_FRIEND_SELECTOR(). See: http://wiki.developers.facebook.com/index.php/Category:FBML_tags 
161  */
162 fb = {};
163 
164 /**
165  * The facebook uid of the current user of this app.
166  * @type {string}
167  */
168 fb.uid = '-1';
169 
170 /**
171  * The current facebook sessionKey. This will only be set if the session user has added the
172  * app and is logged in to it.  Otherwise, it will be undefined.
173  * @type {string}
174  */
175 fb.sessionKey = undefined;
176 if (request.param('fb_sig_session_key')) {
177   fb.sessionKey = request.param('fb_sig_session_key');
178 }
179 
180 /**
181  * List of the uids of the current user's friends.  This will only be filled in
182  * if the session user has added the app and is logged in to it.
183  * @type {array[string]}
184  */
185 fb.friends = [];
186 if (request.param('fb_sig_friends')) {
187   fb.friends = request.param('fb_sig_friends').split(',');
188 }
189 
190 // module private vars
191 var _isSetup = true;
192 
193 /**
194  * @ignore
195  */
196 var _config = {}
197 _config.canvasUrl = null;
198 _config.secret = null;
199 _config.apiKey = null;
200 
201 
202 //----------------------------------------------------------------
203 // fill in some default values
204 //----------------------------------------------------------------
205 if (request.param('fb_sig_user')) {
206   fb.uid = request.param('fb_sig_user');
207 }
208 
209 ['secret', 'apiKey', 'canvasUrl'].forEach(function(k) {
210   if (_getstorage(k)) {
211     _config[k] = _getstorage(k);
212   } else {
213     _isSetup = false;
214   }
215 });
216 
217 /**
218  * contains "http://apps.facebook.com/<the_canvasl_url>/"
219  * @type {string}
220  */
221 fb.fullCanvasUrl = "";
222 
223 if (_config.canvasUrl) {
224   fb.fullCanvasUrl = 'http://apps.facebook.com/' + _config.canvasUrl + '/';
225 }
226 
227 //----------------------------------------------------------------
228 // Utilities
229 //----------------------------------------------------------------
230 
231 // calculates the hash (md5 digest) of a map, according
232 // to Facebook's auth rules
233 function _objectHash(obj) {
234   var kk = keys(obj);
235   kk.sort();
236   var sigstring = "";
237   kk.forEach(function(k) { sigstring += (k + "=" + obj[k]); });
238   sigstring += _config.secret;
239   return md5(sigstring);
240 }
241 
242 /**
243  * Tells facebook to redirect the user to a new URL.  For FBML facebook apps, use
244  * this instead of response.redirect(), so that it redirects the facebook user
245  * instead of the facebook server.
246  * @param {string} [url] The new URL to direct the user to.  If you do not set this parameter,
247  *                       the user will be redirected to the main canvas page.
248  */
249 fb.redirect = function(url) {
250   if (!url) {
251     url = fb.fullCanvasUrl;
252   }
253   if (request.param('fb_sig_in_canvas') == "1") {
254     print(FB_REDIRECT({url: url}));
255     response.stop(true);
256     return;
257   }
258   if (url.match(/^https?:\/\/([^\/]*\.)?facebook\.com(:\d+)?/i)) {
259     // make sure facebook.com url's load in the full frame so that we don't
260     // get a frame within a frame.
261     print(raw('<script type="text/javascript">\ntop.location.href = "'
262 	      +url+'";\n</script>'));
263     response.stop(true);
264     return;
265   }
266   response.redirect(url);
267 };
268 
269 /**
270  * Ensures that the current user has added this app.  If the current user of the app
271  * has already added it, this does nothing.  Otherwise, it immediately redirects the
272  * user to the add page.
273  */
274 fb.requireAdd = function() {
275   if (request.param('fb_sig_added') != '1') {
276     fb.redirect("http://www.facebook.com/add.php?api_key="+_config.apiKey);
277   }
278 };
279 
280 //----------------------------------------------------------------
281 // init()
282 //----------------------------------------------------------------
283 
284 /**
285  * Every facebook app should call fb.init() immediately after import("facebook").
286  * It will first make sure the AppJet app is configured as a valid facebook app.  Next, if
287  * the request path starts with "/callback/", it will
288  * authenticate that the request from facebook and then set up all the fb.* properties.
289  */
290 fb.init = function() {
291   _checkSetup(); // will stop request if preview or not set up
292 
293   if (request.isGET && request.path == '/') {
294     // web request -- not a facebook-proxied request
295     print(P("This is a facebook application.  Visit it at: ",
296 	    A({href: fb.fullCanvasUrl}, fb.fullCanvasUrl)));
297     response.stop(true);
298   }
299   
300   page.setMode('facebook');
301 
302   if (request.isPOST && /^\/callback\//.test(request.path)) {
303     if (!fb.authenticateRequest()) {
304       print(FB_DASHBOARD(),
305 	    FB_ERROR(FB_MESSAGE("Authentication Error: Forbidden"),
306 		     "We could not verify that this request came from Facebook. ",
307 		     "For your safety and privacy, we will not display the page.  ",
308 		     BR(),
309 		   "Please report this to the creators of the application."));
310       response.setStatusCode(403);
311       response.stop(true);
312     }
313   }
314 };
315 
316 //----------------------------------------------------------------
317 // Facebook REST functions
318 //----------------------------------------------------------------
319 
320 // TODO: catch error codes and return them somehow and/or throw exceptions with useful messages.
321 
322 /**
323  * Executes a remote facebook API call.  See the <a
324  * href="http://wiki.developers.facebook.com/index.php/API">Facebok API
325  * Methods</a> wiki page for a list of all methods.
326  *
327  * @param {string} cmd The name of the command, e.g. "facebook.profile.setFBML".
328  * @param {object} params The set of parameters to pass to this call.  Note: api_key, sesion_key,
329  *                 call_id, v, and format, and sig will be automatically set for you.  However, you
330  *                 can override any of these if you like.
331  * @return {object} The call result, as a javascript object.
332  *
333  * @example
334 fb.requireAdd();
335 print(fb.call("users.getInfo",
336               { uids: [700577, 550505927],
337                 fields: ["birthday", "about_me"] }));
338  *
339  */
340 fb.call = function(cmd, params) {
341   if ((! fb.sessionKey) && (!params.session_key)) {
342     throw new Error("No session key for fb.call(); to make calls you should"
343                     +" require that the user be logged in or have added your"
344                     +" app, OR you can use your own infinite session key"
345                     +" for the call.");
346   }
347   function formatParam(p) {
348     if (p instanceof Array) {
349       return p.join(',');
350     } else {
351       return p.toString();
352     }
353   }
354   var params2 = {};
355   params2.method = cmd;
356   params2.api_key = _config.apiKey;
357   params2.session_key = fb.sessionKey;
358   params2.call_id = (+(new Date()));
359   params2.v = "1.0";
360   params2.format = "JSON";
361   eachProperty(params, function(k, v) {
362     params2[k] = formatParam(v);
363   });
364   params2.sig = _objectHash(params2);
365   
366   postResult = wpost("http://api.facebook.com/restserver.php", params2);
367   if (postResult.substr(0, 6).toLowerCase() == '<?xml ') {
368     print("Facebook API Error: ", PRE(postResult));
369     return null;
370   }
371   // decided to use "eval" parse the JSON for now, since
372   // it's coming from facebook and can't do harm if it follows the spec
373   var result = eval("("+postResult+")");
374   if (result.error_msg && result.error_code) {
375     print("Facebook API Error: ");
376     print(result);
377   }
378   return result;
379 };
380 
381 /**
382  * Convenience function for sending a notification to 1 or more users.
383  *
384  * See: <a href="http://developers.facebook.com/documentation.php?v=1.0&method=notifications.send">
385  *  Facebook's docs for facebook.notifications.send</a>.
386  *
387  * @param {array or string} users Array of uids or single uid of those to receive this notification.
388  * @param {string} notificationFBML The FBML content of the notification.
389  */
390 fb.sendNotification = function(users, notificationFBML) {
391   var userStr = users;
392   if (Array.prototype.isPrototypeOf(users)) {
393     userStr = users.join(',');
394   }
395   fb.call("facebook.notifications.send",
396           { to_ids: userStr, notification: notificationFBML });
397 };
398 
399 /**
400  * Convenience function for setting a user's profile FBML.
401  *
402  * See <a
403  *  href="http://developers.facebook.com/documentation.php?v=1.0&method=profile.setFBML">Facebook's
404  *  docs for facebook.profile.setFBML</a>.
405  *
406  * @param {string} uid The uid of the profile to set.
407  * @param {string} markup The text of the FBML to set.
408  * @example
409 fb.requireAdd();
410 fb.setProfileFBML(fb.uid,
411     DIV(CENTER(
412         "Here is another picture of me:",
413         BR(),
414         FB_PROFILE_PIC({uid: fb.uid}))));
415  */
416 fb.setProfileFBML = function(uid, markup) {
417   if (markup.toHTML && typeof(markup.toHTML) == "function") {
418     markup = markup.toHTML();
419   }
420   fb.call("facebook.profile.setFBML",
421 	  { uid: uid, markup: markup });
422 };
423 
424 //----------------------------------------------------------------
425 // Facebook Request Authentication
426 //----------------------------------------------------------------
427 
428 /**
429  * Checks post parameters against fb_sig to verify that the reqest
430  * came from facebook servers and not a haxxor. Note: this method is called automatically by
431  * fb.init(), so you usually do not need to call it directly.
432  *
433  * @return {boolean} true if the request is from facebook and legit; false otherwise.
434  */
435 fb.authenticateRequest = function() {
436   var fbParams = {};
437   for(p in request.params) {
438     if (p.match("^fb_sig_")) {
439       fbParams[p.substring("fb_sig_".length)] = request.params[p];
440     }
441   }
442   var fbCorrectSig = _objectHash(fbParams);
443   return (fbCorrectSig == request.param("fb_sig"));
444 };
445 
446 //----------------------------------------------------------------
447 // config setup
448 //----------------------------------------------------------------
449 
450 // stops the request if this is a preview or the app is not setup
451 function _checkSetup() {
452 
453   var isSetup = (_isSetup === true);
454   
455   // security
456   if (!appjet.isPreview) {
457     if (! isSetup) {
458       print(P("This is an unconfigured facebook app.  If you are the app developer,",
459 	      " preview this app in the AppJet IDE to configure it."));
460       response.stop(true);
461     } else {
462       return; // not preview mode AND app is setup... proceed to render the app
463     }
464   }
465 
466   page.head.write(STYLE(raw("""
467 #container { height: 100%; position: relative; overflow: auto; }
468 iframe#newapp { width: 800px; border: 0; height: 2500px; position: absolute;
469   left: 150px; top: 0px; overflow: hidden; z-index: 1 }
470 #appjetfooter { display: none; }
471 #instrucs { position: absolute; left: 0; top: 0; width: 300px; z-index: 2; background: #ddd;
472     height: 2000px; }
473 #instrucsinner { padding: 0 1em; margin-top: 1em; }
474 #instrucs p { margin: 1em 0; }
475 #instrucs h2 { font-size: 110%; margin: 0; line-height: 120%;
476 text-align: center; padding: 0.5em; background: #666;
477 padding-top: 0.8em; color: #fff; }
478 .notpublished { color: #a00; }
479 .instrucPanel { display: none; }
480 #needLoginPanel { display: block; }
481 .buttons .prev { float: left; }
482 .buttons .next { float: right; }
483 .url { font-size: 90%; }
484 .forcopy { border: 1px solid #999; padding: 5px; }
485 hr { clear: both; margin-top: 0.8em; margin-bottom: 0.8em; }
486 #configurePanel .field { font-size: 90%; }
487 li { margin: 0.5em; }
488 body { line-height: 120%; }
489 .small { font-size: 80%; }
490 """)));
491   
492   if (request.path == "/wizard") {
493     /**** begin wizard ****/
494 
495     var myAppsPath = "http://www.facebook.com/developers/apps.php";
496     var newAppPath = "http://www.facebook.com/developers/editapp.php?new";
497     
498     page.head.write(STYLE(raw("""
499 html, body { height: 100%; overflow: hidden; }
500 body { margin: 0; padding: 0; }
501 """)));
502     
503     function prevNext(prevPanel, nextPanel, opts) {
504       var prevButton = INPUT({className:"prev", type:"button", value:"<< Prev"});
505       if (prevPanel) {
506 	prevButton.attribs.onClick = "selectPanel('"+prevPanel+"');";
507       }
508       else {
509 	prevButton.attribs.disabled = "disabled"; 
510       }
511       var nextButton = INPUT({className:"next", type:"button", value:"Next >>"});
512       if (nextPanel) {
513 	nextButton.attribs.onClick = "selectPanel('"+nextPanel+"');";
514       }
515       else {
516 	nextButton.attribs.disabled = "disabled"; 
517       }
518       if (opts && opts.disablePrev) prevButton.attribs.disabled = "disabled";
519       if (opts && opts.disableNext) nextButton.attribs.disabled = "disabled";
520       if (opts && opts.prevJS && prevButton.attribs.onClick)
521 	prevButton.attribs.onClick = prevButton.attribs.onClick+" "+opts.prevJS;
522       if (opts && opts.nextJS && nextButton.attribs.onClick)
523 	nextButton.attribs.onClick = nextButton.attribs.onClick+" "+opts.nextJS;
524       if (opts && opts.noNext) nextButton = DIV();
525       else if (opts && opts.replaceNextWith) nextButton = opts.replaceNextWith;
526       if (opts && opts.noPrev) prevButton = DIV();
527       else if (opts && opts.replacePrevWith) prevButton = opts.replacePrevWith;
528       
529       return DIV({className:"buttons"}, prevButton, nextButton);
530     }
531     
532     function needLoginPanel() {
533       return DIV({className:"instrucPanel selectedPanel",id:"needLoginPanel"},P(raw("""
534 If you're not already logged into Facebook, you'll be prompted here.  (Your password
535 is not sent to AppJet.)""")),
536 		 P("Facebook may also ask permission to add its Developer interface to your account."),
537 		 P("When you see ",STRONG("My Applications")," click Next."),
538 		 prevNext(null,"creationPanel"));
539     }
540     
541     function creationPanel() {
542       return DIV({className:"instrucPanel",id:"creationPanel"},
543 		 P({id:"createappcont"},
544 		   STRONG("Click me: "),INPUT({id:"createapp", type:"button", value:"Start New Facebook App",
545 					       onClick:"loadInIframe('"+newAppPath+"'); selectPanel('appNamePanel');"})),
546 		 P("Or, click Edit Settings on an app to the right."),
547 		 prevNext("needLoginPanel","appNamePanel"));      
548     }
549 
550     function appNamePanel() {
551       return DIV({className:"instrucPanel",id:"appNamePanel"},
552 		 P(STRONG("Application Name:")," Choose a name for your app on Facebook."),
553 		 P("Remember to mark the checkbox if necessary."),
554 		 P("Expand ",STRONG("Optional Fields")," so that the triangle points down."),
555 		 prevNext("creationPanel","baseOptsPanel"));
556     }
557     
558     function getCallbackURL() {
559       return "http://"+appjet.appName+"."+appjet.mainDomain+"/callback/";
560     }
561     
562     function baseOptsPanel() {
563       return DIV({className:"instrucPanel",id:"baseOptsPanel"},
564 		 P(STRONG("Callback URL:")," Enter this:\n",SPAN({className:"url forcopy"},
565 								 getCallbackURL())),
566 		 P(STRONG("Canvas Page URL:")," Choose one, enter it to the right, and tell me here:\n",
567 		   SPAN({className:"url"},"http://apps.facebook.com/",
568 			INPUT({type:"text",id:"canvasPage",size:10,onkeypress:
569 			       "typeCanvasPageNotify();",onclick:"typeCanvasPageNotify();"}),"/")),
570 		 prevNext("appNamePanel","installOptsPanel",{disableNext:true,
571 							     nextJS:"typeCanvasPageNotify();"}));
572     }
573     
574     function installOptsPanel() {
575       return DIV({className:"instrucPanel",id:"installOptsPanel"},
576 		 P(STRONG("Can your application be added?"),' Click "Yes".'),
577 		 P("(Scroll down...)"),
578 		 P(STRONG("Who can add your application?"),' Mark "Users".'),
579 		 P(STRONG("Post-Add URL:"),"\n",
580 		   SPAN({className:"url forcopy"},"http://apps.facebook.com/",
581 			SPAN({className:"canvasPageCopied"},"<canvasPage>"),"/")),
582 		 P(STRONG("Side Nav URL:"),"\n",
583 		   SPAN({className:"url forcopy"},"http://apps.facebook.com/",
584 			SPAN({className:"canvasPageCopied"},"<canvasPage>"),"/")),
585 		 prevNext("baseOptsPanel","submitPanel"));
586     }
587     
588     function submitPanel() {
589       return DIV({className:"instrucPanel",id:"submitPanel"},
590 		 P("Those are the important settings."),
591 		 P(STRONG("When you're ready, click the blue Submit button.")),
592 		 P("I'll need some information from the next page..."),
593 		 prevNext("installOptsPanel","configurePanel"));
594     }
595     
596     function configurePanel() {
597       return DIV({className:"instrucPanel",id:"configurePanel"},
598 		 FORM({action:"/config",method:"post"},
599 		      A({href:"javascript:void(loadInIframe('"+myAppsPath+"#content'));"}, "Scroll to top of My Applications"),
600 		      HR(),
601 		      P("Find the listing for your app and copy the following:  "),
602 		      P({className:"field"},STRONG("API Key:"),"\n",
603 			INPUT({type:"text",value:"",size:"32",name:"apikey"})),
604 		      P({className:"field"},STRONG("Secret:"),"\n",
605 			INPUT({type:"text",value:"",size:"32",name:"secret"})),
606 		      HR(),
607 		      P("Confirm the following:"),
608 		      P({className:"field"},STRONG("Callback URL:"),"\n",
609 			getCallbackURL()),
610 		      P({className:"field"},STRONG("Canvas Page (from earlier):"),"\n",
611 			"http://apps.facebook.com/",INPUT({id:"canvasPageConfig", type:"text",
612 							   value:"XXXX", name:"canvaspage", size:10}),"/"),
613 		      HR(),
614 		      prevNext("submitPanel",null,{replaceNextWith:
615 						   INPUT({className:"next",type:"submit",value:"Done!"})})));
616     }
617 
618     page.setTitle("Facebook App Wizard");
619     page.head.write(SCRIPT(
620       {src:
621        "http://cachefile.net/scripts/jquery/1.2.1/jquery-1.2.1.js"}));
622     print(DIV({id:"container"},
623 	      IFRAME({id:"newapp",name:"newapp",
624 		      src:myAppsPath})),
625 	  DIV({id:"instrucs"},H2("Facebook App Configuration"),
626 	      DIV({id:"instrucsinner"},
627 		  needLoginPanel(), creationPanel(),
628 		  appNamePanel(),
629 		  baseOptsPanel(), installOptsPanel(),
630 		  submitPanel(), configurePanel())));
631     
632     page.head.write(SCRIPT(raw("""
633 function selectPanel(theId) {
634     $(".instrucPanel").hide().removeClass("selectedPanel");
635     $("#"+theId).show().addClass("selectedPanel");
636 }
637 
638 function getCanvasPage() {
639     var cp = $("input#canvasPage").get(0).value;
640     cp = cp.replace(/^\s+/,'').replace(/\s+$/,'');
641     return cp;
642 }
643 
644 function typeCanvasPageNotify() {
645     $('#baseOptsPanel .next').get(0).disabled = false;
646     var canvasPage = getCanvasPage();
647     $(".canvasPageCopied").html(canvasPage);
648     $("#canvasPageConfig").get(0).value = canvasPage;
649 }
650 
651 function loadInIframe(url) {
652   $('iframe#newapp').get(0).contentWindow.location.href = url;
653 }
654 """)));
655     /**** end wizard ****/
656   }
657   else if (request.path == "/config" && request.isPost) {
658     _setstorage('apiKey', trim(request.params.apikey));
659     _setstorage('secret', trim(request.params.secret));
660     _setstorage('canvasUrl', trim(request.params.canvaspage));
661     print(SCRIPT(raw("window.close();")));
662   }
663   else {
664     // path "/", etc.
665     if (! isSetup) {
666       print(H2("Facebook Library: Unconfigured App"));
667       if (! appjet._native.hasBeenPublished()) {
668 	print(P(SPAN({className:"notpublished"},'First, you have to give this app a public URL.')));
669 	print(UL(LI('Click the "Publish" tab '+
670 		    'above and follow the instructions.'),
671 		 LI('Then come back and reload this Preview pane.')));
672       }
673       else {
674 	print(P("Click to open the ",A({href:"/wizard", target:"_blank"},"Facebook App "+
675 				       "Wizard")," in a new window."));
676 	print(P("When you're finished, click to ",
677 		link("/","refresh")," this page."));
678       }
679     }
680     else {
681       // is set-up
682       print(H2("This is a configured Facebook app."));
683       print(P("(To reconfigure this app, use the ",
684 	      A({href:"/wizard", target:"_blank"},"Facebook App "+
685 		"Wizard"),".)"));
686     }
687   }
688   
689   response.stop(true);
690 };
691 
692