/* This web application demonstrates using cookies and a database to store state. */ webapp cookie2; // use the SQLite database; it's easiest. import SQLiteDB; import DB; import Time; import Webapp; import HTMLDocument; // this stores the user state data User(Int userid, Int visited); globals { // this isn't ideal, but it makes the rest of the code simpler HTMLDocument doc; SQLiteDB db = connect("/users/kaya/share/databases/cookie2"); } HTMLDocument webmain() { // this is a useful thing to do when testing // but should be commented out for a real application // enableDebugging(); doc = new(HTML4Strict,"Cookie demonstration"); try { // Create the sessions table if it doesn't exist // This is just to make this example entirely self-contained. // You'd probably want to do this differently in a real application void(exec(db,"CREATE TABLE IF NOT EXISTS Sessions (user int, session text, lastused int)")); // for simplicity, the entire page is generated in the handler // in a real application, there'd probably be some template code // above and below this call try { key = incomingValue("cookie_demo_login",DataCookie); } catch(e) { key = ""; } appendExisting(doc.body, retrieveFunction(@notLoggedIn, cookieRetriever, key)); } catch(SQLiteError(err)) { // only display details of the error to the browser when debugging // to help security if (debugMode()) { void(addParagraph(doc.body,"Database error: "+err)); } else { void(addParagraph(doc.body,"Database error: sorry")); // in a real application it might be nice to log the error // details somewhere } } source = addParagraph(doc.body,"Read the "); void(appendInlineElement(source,Hyperlink("../files/cookie2.k"),"source code")); // close the database now close(db); return doc; } // this is the default function if the cookie doesn't exist. ElementTree notLoggedIn() { // if the form has been submitted if (incomingExists("username",DataPost)) { // then log in user = Int(incomingValue("username",DataPost)); // and switch to the logged-in handler if (user > 0) { return loggedIn(User(user,0)); } } // otherwise display the log in form content = anonymousBlock; void(addHeading(content,1,"Please log in")); void(addParagraph(content,"You are not logged in. Please make up a user ID (numbers only) and log in to see the rest of this application.")); form = addLocalForm(content); f1 = addFieldset(form,"Log in"); void(addLabelledInput(f1,"User ID",InputText,"username","",10)); void(addRemoteControlInput(f1,InputSubmit,"","Log in")); return content; } ElementTree loggedIn(User user) { if (incomingExists("logout",DataGet) && !incomingExists("username",DataPost)) { // erase the login cookie if (incomingExists("logout",DataGet)) { addHTTPHeader(doc,setCookie("cookie_demo_login","")); } // the second half of the condition should always be false, but // if it's not included, someone could mischievously send the // application into an infinite loop. return notLoggedIn(); // anyway, if they're logging out, print the log in form // we've already deleted the cookie above. } else { // but usually, it'll just be a page. content = anonymousBlock(); // keep track of the number of pages visited, just to prove that // state can be updated as the user navigates the application user.visited++; void(addParagraph(content,"Hello, user "+user.userid+". You have viewed "+user.visited+" pages this session.")); // while logged in, the page parameter says what page to view. if (incomingExists("page",DataGet)) { pageid = Int(incomingValue("page",DataGet)); } else { pageid = 100; } // now show the page-specific content showPage(content,pageid); logout = addParagraph(content,"When you're finished, "); void(appendInlineElement(logout,Hyperlink(webappName()+"?logout"),"log out")); void(storeFunction(cookieStorer@(doc,user.userid),@loggedIn,user)); return content; } } Void linkToPage(ElementTree list, Int linkid) { li = pushListItem(list,"Go to "); void(appendInlineElement(li,Hyperlink(webappName()+"?page="+linkid),"page "+linkid)); } Void showPage(ElementTree content, Int pageid) { cpage = addParagraph(content,"You are now on page "+pageid+"."); if (pageid == 1) { addString(cpage," Well done - you found page 1! You can log out and try to find it again in fewer moves if you're really bored."); // Exercise for the reader: modify this application so that a visitor // can't cheat and go straight to page 1 by changing the URL. Fixing // things like this is a crucial part of web application security. // Hint: you will probably need to expand the state data type a bit } else { addString(cpage," If you get bored, try to find page 1."); } // add some navigation to simulate links to the next stages of the // application. ul = addList(content,Unordered,0); if (pageid % 2 == 0) { linkToPage(ul,pageid/2); } else { linkToPage(ul,(pageid*7)+1); } if (pageid % 5 == 1) { linkToPage(ul,pageid+4); } else { linkToPage(ul,pageid+10); } if (pageid % 3 == 0) { linkToPage(ul,(pageid*2)-1); } else if (pageid % 3 == 1) { linkToPage(ul,pageid-13); } else { linkToPage(ul,pageid/3); } if (pageid < 10) { linkToPage(ul,pageid*pageid); } else if (pageid > 1000) { linkToPage(ul,pageid/20); } } String cookieStorer(HTMLDocument hdoc, Int uid, String state) { // just store the username in the state key = encode(String(uid)); // encrypt it to stop easy takeover of other accounts cookie = setCookie("cookie_demo_login",key); addHTTPHeader(hdoc,cookie); // state is base64-encoded and uid is an integer, so they're safe // but in general you need to escape Strings before placing them // into SQL queries to prevent SQL injection attacks // also delete sessions more than an hour old to save database space void(exec(db,"DELETE FROM Sessions WHERE user = '"+uid+"' OR lastused+3600 < '"+now()+"'")); void(exec(db,"INSERT INTO Sessions (user,session,lastused) VALUES ('"+uid+"','"+state+"','"+now()+"')")); return key; } Exception SessionExpired; String cookieRetriever(String key) { // if the key isn't a valid key, then this will throw an Exception // another good reason to encrypt them uid = Int(decode(key)); getstate = exec(db,"SELECT session FROM Sessions WHERE user = '"+uid+"'"); if(getstate.rows == 0) { // maybe the session has expired throw(SessionExpired); } else { return string(getstate.table[0][0]); } }