WebFund 2015W Final Exam Review

From Soma-notes

Audio

The audio from the exam review given on April 10, 2015 is now available. (Note: the file does not play in Firefox unfortunately.)

Notes

The final exam will be based on exam-notes, a simplified version of the solutions to tls-notes from Assignment 10.

There will be very little in terms of code that you must write on the exam, no more than a few lines for small implementations. While your answers can be pure code, it is better to provide written answers with high level explanations.

Review of the different contexts for functions (the main difference between these contexts is the use of the keyword 'this'):

  • Regular function call - keyword 'this' refers to the global object
  • Method call - keyword 'this' refers to the object for which the method was called
  • Constructor call - keyword 'this' refers to the object being constructed
  • Apply call - keyword 'this' refers to the parameter supplied to the apply() call indicating what the function is being applied to

Differentiation between client side and server side

On the client, HTML is rendered by the browser and used to construct the DOM. JavaScript can also be provided to be run inside the browser and interact with the DOM.

On the server, JavaScript is run as an application to provided all necessary server functionality.

Communications between client and server

The two sides (client and server) talk to each other using HTTP. HTTP is built on top of other technologies but it is essentially just structured data being sent back and forth.

A server cannot initiate communication by sending a response to a client. The client must first send a request to the server, the server may then respond to that request. Requests are sent to a server due to actions taken by a client such as trying to load a page or a user clicking on a button which submits a form. A response typically contains HTML, CSS or JavaScript code. A response may cause subsequent requests to be made in order to obtain linked resources (images, stylesheets, scripts, etc).

Older servers (static servers)

On an older web server, you would have files such as HTML documents, images, stylesheets, etc. These are static resources which could be served 'as is' by the server. A new web server such as one implemented in Node can easily serve resources as a static web server but its main benefit is the ability to run additional code when a request comes in. JavaScript can be used in this context to generate HTML or do many other things.

JavaScript execution environments

Node.js (run on the server) is an execution environment for JavaScript. Similarly, there is an execution environment for JavaScript in web browsers to allow for the client to run JavaScript code. These executions environments have differences between them. The browser environment is sandboxed so that it cannot access the rest of the computer (file I/O, networking).

JavaScript run in the browser is meant for two main purposes:

  • Manipulation of the DOM - Changing the structure of the page, updating the interface, etc...
  • Sending AJAX requests (asynchronous GET and POST requests) - This allows the client to communicate with the server in the background, bypassing the need to use HTML elements to trigger communications as well as the need to reload a page when receiving data.

Exam code

Differentiation between client side code and server side code

  • The code in index.js is all run on the server.
  • Anything in the public folder is sent directly to the client. Scripts and HTML code sent from this folder are run on the client.
  • Jade templates are rendered in HTML on the server and then sent to the client to have the HTML rendered by the browser.

What does register.js do?

First, recall that $ refers to jQuery, it represents the jQuery object. By providing a single parameter to jQuery as a function, we are defining an on-load function. This means that the function is run once the page is fully loaded. The first line in this function uses jQuery to find an HTML element with an ID of 'register' and calls to on() method of the element to define a function for a click event. The function finds a form element in the DOM and changes its action from /login to /register and then submits the form.

Other notes and tips

You must fully understand the definition of routes that occurs in index.js. The router methods such a get() and post() are run only once when the index.js file is run during the startup of the server. These methods register a callback function for the specified route so that whenever a request comes to the server for that route, the callback function is run. The callback function is not run until such a request comes in and so it could potentially never be run if a request never comes in for that route.

It is important to recognize that functions are treated as first class objects in JavaScript. They can be treated just as other objects can, meaning that they can even be used as parameters for another function.

Notice that the exam code does not use bcrypt. The passwords are stored in plain text. This makes the code much simpler but it is a very bad practice. This makes your database very vulnerable as all passwords are completely visible in plain text if anyone happens to get access to the database. You should never do this.

Recall that hashing and encryption are different. With encryption, you can decrypt the data back to its original format if you have the proper key. With hashing, you are producing a fixed length representation of the data based on a hashing function. You do not expect to be able to retrieve data that has been hashed, only verify that something matches what you expect.

Expect questions on the exam about what causes certain requests and what type of response you would expect the server to produce and why.

Code

routes/index.js

var express = require('express');
var router = express.Router();
var mongodb = require('mongodb');
var mc = mongodb.MongoClient;
var ObjectID = mongodb.ObjectID;

var notesCollection, usersCollection;

mc.connect('mongodb://localhost/exam-notes', function(err, db) {
    if (err) {
        throw err;
    }
    
    notesCollection = db.collection('notes');
    usersCollection = db.collection('users');
});

router.post('/register', function(req, res) {
    var username = req.body.username;
    var password = req.body.password;

    var checkInsert = function(err, newUsers) {
        if (err) {
            res.redirect("/?error=Unable to add user");
        } else {
            res.redirect("/?error=User " + username +
                         " successfully registered");
        }
    }

    var checkUsername = function(err, user) {
        if (err) {
            res.redirect("/?error=unable to check username");
        } else if (user === null) {
            var newUser = {
                username: username,
                password: password
            };
            usersCollection.update({username: username},
                                   newUser,
                                   {upsert: true},
                                   checkInsert);    

        } else {
            res.redirect("/?error=user already exists");
        }
    }
    
    usersCollection.findOne({username: username}, checkUsername);
});

router.get('/', function(req, res) {
    if (req.session.username) {
        res.redirect("/notes");
    } else {
        res.render('index', { title: 'COMP 2406 Exam Notes Demo', 
                              error: req.query.error });
    }
});

router.get('/notes', function(req, res) {
    var username = req.session.username;

    if (username) {
        res.render("notes.jade", {username: username,
                                  title: username +"'s Notes"});
    } else {
        res.redirect("/?error=Not Logged In");
    }
});

router.post('/login', function(req, res) {
    var username = req.body.username;
    var password = req.body.password;
    
    var authenticateUser = function(err, user){
        if (err || user === null || password !== user.password) {
            res.redirect("/?error=invalid username or password");       
        } else {
            req.session.username = username;
            res.redirect("/notes");
        }
    }
    
    usersCollection.findOne({username: username}, authenticateUser);
});

router.post('/logout', function(req, res) {
    req.session.destroy(function(err){
        if (err) {
            console.log("Error: %s", err);
        }
    });
    res.redirect("/");
});

router.get('/getNotes', function(req, res) {
    var username = req.session.username;

    var renderNotes = function(err, notes) {
        if (err) {
            notes = [{"title": "Couldn't get notes",
                      "owner": username,
                      "content": "Error fetching notes!"}];
        }
        res.send(notes);
    }
    
    if (username) {
        notesCollection.find({owner: username}).toArray(renderNotes);
    } else {
        res.send([{"title": "Not Logged In",
                   "owner": "None",
                   "content": "Nobody seems to be logged in!"}]);
    }    
});

router.post('/updateNote', function(req, res) {
    var username = req.session.username;
    var id = req.body.id;
    var title = req.body.title;
    var content = req.body.content;
    
    var checkUpdate = function(err, result) {
        if (err) {
            res.send("ERROR: update failed");
        } else {
            res.send("update succeeded");
        }
    }
    
    if (username) {
        if (id && title && content) {
            notesCollection.update({_id: ObjectID(id)},
                                   {$set: {title: title,
                                           content: content}},
                                   checkUpdate);
        } else {
            res.send("ERROR: bad parameters");
        }
    } else {
        res.send("ERROR: not logged in");
    }
});

router.post('/newNote', function(req, res) {
    var username = req.session.username;
    var newNote;

    var reportInserted = function(err, notesInserted) {
        if (err) {
            res.send("ERROR: Could not create a new note");
        } else {
            res.send(notesInserted[0]._id);
        }
    }

    if (username) {
        newNote = {title: "Untitled",
                   owner: username,
                   content: "No content"};

        notesCollection.insert(newNote, reportInserted);
    } else {
        res.send("ERROR: Not Logged In");
    }
});

module.exports = router;

public/javascripts/register.js

$(function(){
    $("#register").on("click",function(){
        var $form = $("form");
        $form.attr("action", "/register");
        $form.submit();
    });
});

public/javascripts/notes.js

$(function() {
    var insertNotesIntoDOM = function(userNotes) {
        var i, theNote;

        $(".notes").remove();

        if (userNotes.length < 1) {
            $("#notesList").append('<div class="notes">No notes!</div>');
            return;
        }

        for (i=0; i < userNotes.length; i++) {
            console.log("Adding note " + i);
            theNote = userNotes[i];
            $("#notesList").append('<div class="notes list-group-item ' +
				   'list-group-item-default"> ' +
                                   '<a href="#" ' + 
                                   'class="notes list-group-item-heading" ' +
                                   'id="edit' + i + '">' +
                                   '<h4>' + theNote.title + '</h4></a> ' +
                                   '<p class="list-group-item-text">' + 
                                   theNote.content + '</p>' +
                                   '</div>');
            $("#edit" + i).click(userNotes[i], editNote);
        }
    }

    var getAndListNotes = function() {
        $.getJSON("/getNotes", insertNotesIntoDOM);
    }

    var updateNoteOnServer = function(event) {
        var theNote = event.data;
        theNote.title = $("#noteTitle").val();
        theNote.content = $("#noteContent").val();
        noteListing();
        $.post("/updateNote",
               {"id": theNote._id,
                "title": theNote.title,
                "content": theNote.content},
               getAndListNotes);
    }

    var editNote = function(event) {
        var theNote = event.data;

        $(".notesArea").replaceWith('<div class="notesArea" ' +
                                    'id="editNoteDiv"></div>');
        $("#editNoteDiv").append('<h2>Edit Note</h2>');

        $("#editNoteDiv").append('<div><input type="text" ' +
                                 'id="noteTitle"></input></div>');
        $("#noteTitle").val(theNote.title);

        $("#editNoteDiv").append('<div>' +
                                 '<textarea cols="40" rows="5" ' +
                                 'id="noteContent" wrap="virtual">' +
                                 '</textarea></div>');
        $("#noteContent").val(theNote.content);


        $("#editNoteDiv").append('<button type="button" ' + 
                                 'class="btn btn-success" ' +
                                 'id="updateNote">' +
                                 'Update</button>')
        $("#updateNote").click(theNote, updateNoteOnServer);
    }
    
    var editNewNote = function(result) {
        var theNote = {"title": "",
                       "content": ""};

        if (result.indexOf("ERROR") === -1) {
            theNote._id = result;
            editNote({data: theNote});
        } else {
            noteListing();
        }
    }

    var newNote = function() {
        $.post("/newNote", editNewNote);
    }

    var noteListing = function() {
        $(".notesArea").replaceWith('<div class="notesArea" ' +
                                    'id="listNotesDiv"></div>');

        $("#listNotesDiv").append('<h2>Notes</h2>');
        $("#listNotesDiv").append('<div id="notesList" class="list-group well">' +
                                  '<a class="notes list-group-item">Loading Notes...</a>' +
                                  '</div>');

        $("#listNotesDiv").append('<button id="newNote"' +
                                  ' class="btn btn-primary"' +
                                  ' type="button">' +
                                  'New Note</button>');
        $("#newNote").click(newNote);

        getAndListNotes();
    }

    noteListing();
});

views/layout.jade

doctype html
html
  head
    title= title
    link(rel='stylesheet', href='/libs/bootstrap/css/bootstrap.css')
    link(rel='stylesheet', href='/libs/bootstrap/css/bootstrap-theme.css')
    link(rel='stylesheet', href='/stylesheets/style.css')
    script(src='/libs/jquery.js')
    script(src='/libs/bootstrap/js/bootstrap.js')
    block header
  body(role="document")
    block content

views/index.jade

extends layout

block header
  script(src="javascripts/register.js")

block content
  h1= title
  p Welcome to #{title}
  - if(error)
    div.error #{error}
  div
    form(action="/login", method="post")
        div.form-group
            label(for="username") Username
            input.form-control.limitwidth(type="text", name="username")
        div.form-group
            label(for="password") Password
            input.form-control.limitwidth(type="password", name="password")
        button.btn.btn-default(type="submit") Login
        button#register.btn.btn-warning(type="button") Register

views/notes.jade

extends layout

block header
  script(src="javascripts/notes.js")

block content
  div.page-header
    h1#pageTitle #{username}&#39;s Account
    p#pageWelcome Welcome #{username}

  form(action="/logout", method="post")
        button.btn.btn-info(type="submit") Logout
  
  div.notesArea

views/error.jade

extends layout

block content
  h1= message
  h2= error.status
  pre #{error.stack}