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