Code-source

./package.json
{
    "name": "forum",
    "version": "1.0.0",
    "dependencies": {
        "express": "4.13.4",
        "express-session": "1.13.0",
        "ejs": "2.4.1",
        "crypto": "0.0.3",
        "base64url": "1.0.6",
        "node-mysql": "0.4.2",
        "socket.io": "1.4.5",
        "mime-types": "2.1.10"
    }
}

Code côté serveur

./app.js
var http = require('http');
var path = require('path');
var crypto = require('crypto');
var base64url = require('base64url');
var mysql = require('mysql');
var express = require('express');
var app = express();
var server = http.Server(app);
var io = require('socket.io')(server);
var express_session = require('express-session');

var routes = require('./serveur/routes.js');
var session = require('./serveur/session.js');
var socket = require('./serveur/socket.js');
var sql = require('./serveur/sql.js');

routes.f(app, session, express, path, __dirname,
  mysql, sql, express_session, crypto, base64url);
socket.f(io, mysql, sql, session, crypto, base64url);

server.listen(8888);
serveur/routes.js
exports.f = function(
  app,
  session,
  express,
  path,
  dossier,
  mysql,
  sql,
  express_session,
  crypto,
  base64url
) {

  // le chiffre 7 pourrait être n'importe quel autre chiffre ou lettre
  var session_secret = base64url(crypto.randomBytes(20)).replace('-', '7')
  session.session_init(app, express_session, session_secret);
  // la session est initiée dans express.js

  app.get('/token/:token', function(req, res) {
    session.conv_jeton(mysql, sql, req.params.token, function(nom_utilisateur) {
      session.login(res, req.session, nom_utilisateur, function() {
        res.redirect(301, '/');
      });
    });
  });

  // page de login
  app.get('/login', function(req, res) {
    res.render('login.ejs', {
      nom_utilisateur: session.session_active(req.session, req)
    });
  });

  // page de logout
  app.get('/logout', function(req, res) {
    session.logout(req.session);
    // redirection vers login
    res.redirect(301, '/login');
  });

  // page de nouveau compte
  app.get('/nouveau_compte', function(req, res) {
    res.render('nouveau_compte.ejs', {
      nom_utilisateur: session.session_active(req.session, req)
    });
  });

  // page de profil
  app.get(/^\/profil_([a-z0-9]{4,10})/i, function(req, res) {
    sql.profil(mysql, sql, req.params[0], function(result) {
        res.render('profil.ejs', {
          nom_utilisateur: session.session_active(req.session, req),
          profil : result
        });
    });
  });

  // page d'accueil
  app.get(/^\/home|\/$/, function(req, res) {
    sql.chargement_discussions(mysql, sql, true, function(discussions) {
      res.render('index.ejs', {
        nom_utilisateur: session.session_active(req.session, req),
        discussions: discussions
      });
    });
  });

  // dernière discussion - utilisée par des requêtes AJAX
  app.get('/derniere_discussion', function(req, res) {
    /* ici, la variable tout_charger vaut false,
    car nous ne voulons que la dernière discussion */
    sql.chargement_discussions(mysql, sql, false, function(discussion) {
      res.render('chargement_discussions.ejs', {
        discussions: discussion
      });
    });
  });

  // page de discussions
  app.get(/^\/([0-9]{1,9})$/, function(req, res) {
    sql.chargement_messages(req.params[0], mysql, sql, true,
      function(discussion_existante, sujet_discussion, messages) {
        if (!discussion_existante) {
          res.render('erreur_discussion.ejs');
        }
        else {
          res.render('discussion.ejs', {
            nom_utilisateur: session.session_active(req.session, req),
            sujet_discussion: sujet_discussion,
            m : messages
          });
        }
      }
    );
  });

  // dernier message (d'une discussion) - utilisé par des requêtes AJAX
  app.get(/^\/dernier_([0-9]{1,9})$/, function(req, res) {
    sql.chargement_messages(req.params[0], mysql, sql, false,
      function(discussion_existante, sujet_discussion, messages) {
        res.render('chargement_messages.ejs', {
          nom_utilisateur: session.session_active(req.session, req),
          m: messages
        });
      }
    );
  });

  // page de nouvelle discussion
  app.get('/new', function(req, res) {
    res.render('new.ejs', {
      nom_utilisateur: session.session_active(req.session, req)
    });
  });

  // fichiers statiques
  app.get(/static\/([0-9a-z\.\/_-]+)$/i, function(req, res) {
    res.sendFile(dossier + '/static/' + req.params[0]);
  });

  // page d'erreur
  app.use(function(req, res, next) {
    res.render('error.ejs');
  });

};
./serveur/session.js
// initialise la session
exports.session_init = function(app, express_session, session_secret) {
    app.use(express_session({
        secret: session_secret,
        name: 'utilisateur',
        resave: true,
        saveUninitialized: true
    }));
};

// crée une nouvelle session pour un utilisateur
exports.login = function(res, sess, nom_utilisateur, callback) {
    sess.nom_utilisateur = nom_utilisateur;
    callback();
};

// supprime la session
exports.logout = function(sess) {
    sess.destroy(function(err) {
    });
};

// retourne la session en cours
exports.session_active = function(sess, req) {
    if (sess.nom_utilisateur) {
        return sess.nom_utilisateur;
    } else {
        return 'session inexistante';
    }
};

// crée un jeton d'authentification, le met dans la base de données, le retourne
exports.creation_jeton =
  function(mysql, sql, base64url, crypto, socket, nom_utilisateur) {
    var token = base64url(crypto.randomBytes(20)).replace('-', '_');
    var requete_sql = '\
        INSERT INTO session(nom_utilisateur, token) VALUES(??, "??")';
    var inserts = [nom_utilisateur, token];
    requete_sql = sql.preparer(mysql, requete_sql, inserts);
    sql.requete(mysql, sql, requete_sql, function() {
        socket.emit('redirection', '/token/'+token);
    });
}

// convertit un jeton en nom d'utilisateur et supprime le jeton de la base de données
exports.conv_jeton = function(mysql, sql, token, callback) {
    var requete_sql = 'SELECT nom_utilisateur FROM session WHERE token = "??"';
    var inserts = [token];
    requete_sql = sql.preparer(mysql, requete_sql, inserts);
    sql.requete(mysql, sql, requete_sql, function(results) {
        var requete_sql = '\
        DELETE FROM session WHERE nom_utilisateur = "'+results[0].nom_utilisateur+'"';
        sql.requete(mysql, sql, requete_sql);
        callback(results[0].nom_utilisateur);
    });
}
./serveur/socket.js
exports.f = function(io, mysql, sql, session, crypto, base64url) {

  io.on('connection', function (socket) {

    // vérifie si le mot de passe est correct
    socket.on('login', function (login) {
      var requete_sql = '\
        SELECT mot_de_passe \
        FROM users \
        WHERE nom_utilisateur = ??';
      var inserts = [login.nom_utilisateur];
        requete_sql = sql.preparer(mysql, requete_sql, inserts);

      sql.requete(mysql, sql, requete_sql, function(results) {
        try {
        /* ce bloc try-catch sert à détecter une erreur pouvant survenir
        lorsqu'on demande le mot de passe d'un utilisateur inexistant */
          if (login.password == results[0].mot_de_passe) {
            session.creation_jeton(
              mysql,
              sql,
              base64url,
              crypto,
              socket,
              login.nom_utilisateur
            );
          } else {
            socket.emit('erreur_login');
          }
        }
        catch(e) {
          socket.emit('erreur_login');
        }
      });
    });

    // crée un nouveau compte
    socket.on('nouveau_compte', function(compte) {
      var requete_sql = 'SELECT \
        COUNT(nom_utilisateur) AS "user_exists"\
        FROM users WHERE nom_utilisateur = ??';
      var inserts = [compte.nom_utilisateur];
      requete_sql = sql.preparer(mysql, requete_sql, inserts);
      sql.requete(mysql, sql, requete_sql, function(result) {
        if (result[0].user_exists !== 0) {
          socket.emit('utilisateur_existant');
          return;
        }
        else {
          var requete_sql = '\
            INSERT INTO \
              users(nom_utilisateur, mot_de_passe, email, nom, prenom, avatar) \
            VALUES(??, ??, ??, ??, ??, ?)';
          var inserts = [
            compte.nom_utilisateur,
            compte.password,
            compte.mail,
            compte.nom,
            compte.prenom,
            Math.floor((Math.random() * 100) + 1)
          ];
          requete_sql = sql.preparer(mysql, requete_sql, inserts);
          sql.requete(mysql, sql, requete_sql);
          session.creation_jeton(
            mysql, sql, base64url, crypto, socket, compte.nom_utilisateur);
        }
      });
    });

    // modifie les coordonnées d'un utilisateur
    socket.on('modifier_compte', function(compte) {
      var requete_sql = '\
        UPDATE users SET email = ??, nom = ??, prenom = ?? \
        WHERE nom_utilisateur = ??';
      var inserts = [
        compte.mail,
        compte.nom,
        compte.prenom,
        compte.utilisateur
      ];
      requete_sql = sql.preparer(mysql, requete_sql, inserts);
      sql.requete(mysql, sql, requete_sql, function(results) {
        socket.emit('mod_compte_ok', {
          mail : compte.mail,
          nom : compte.nom,
          prenom : compte.prenom,
          utilisateur : compte.utilisateur,
          broadcast : false
        });
        socket.broadcast.emit('mod_compte_ok', {
          mail : compte.mail,
          nom : compte.nom,
          prenom : compte.prenom,
          utilisateur : compte.utilisateur,
          broadcast : true
        });
      });
    });

    // modifie le mot de passe d'un utilisateur
    socket.on('modifier_password', function(mod) {
      var requete_sql = '\
        SELECT mot_de_passe \
        FROM users \
        WHERE nom_utilisateur = ??';
      var inserts = [mod.nom_utilisateur];
      requete_sql = sql.preparer(mysql, requete_sql, inserts);

      sql.requete(mysql, sql, requete_sql, function(results) {

        if (mod.ancien_password == results[0].mot_de_passe) {
          var requete_sql = 'UPDATE users SET mot_de_passe = ?? \
            WHERE nom_utilisateur = ??';
          var inserts = [mod.nouveau_password, mod.nom_utilisateur];
          requete_sql = sql.preparer(mysql, requete_sql, inserts);
          sql.requete(mysql, sql, requete_sql);
          socket.emit('mod_password_ok');
        } else {
          socket.emit('ancien_password_incorrect');
        }
      });
    });

    // publie un nouveau message et l'enregistre dans la base de données
    socket.on('nouveau_message', function (message) {
      var requete_sql = '\
        INSERT INTO messages(nom_utilisateur, contenu, \
          date_ecriture, id_discussion, likes)\
        VALUES(??, ??, NOW(), ?, 0)';
      var inserts = [
        message.nom_utilisateur,
        message.contenu,
        message.id_discussion
      ];
      requete_sql = sql.preparer(mysql, requete_sql, inserts);
      sql.requete(mysql, sql, requete_sql);
      socket.emit('charger_dernier_message'+message.id_discussion);
      socket.broadcast.emit('charger_dernier_message'+message.id_discussion);
    });

    // modifie un message et l'enregistre dans la base de données
    socket.on('modification_message', function (d) {
      var requete_sql = '\
        UPDATE messages SET contenu = ??, date_modification=NOW() WHERE id_message = ?';
      var inserts = [d.contenu.replace(/'/g, "\\'"), d.id_message];
      requete_sql = sql.preparer(mysql, requete_sql, inserts);
      sql.requete(mysql, sql, requete_sql, function(results) {
        var requete_sql = '\
          SELECT * FROM messages WHERE id_message = ?';
        var inserts = [d.id_message];
        requete_sql = sql.preparer(mysql, requete_sql, inserts);
        sql.requete(mysql, sql, requete_sql, function(results) {

          socket.emit('update_message'+d.id_discussion, {
            id_message : results[0].id_message,
            date_modification : results[0].date_modification,
            contenu : results[0].contenu
          });
          socket.broadcast.emit('update_message'+d.id_discussion, {
            id_message : results[0].id_message,
            date_modification : results[0].date_modification,
            contenu : results[0].contenu
          });
        });
      });
    });

    // supprime un message (dans la base de données)
    socket.on('suppression_message_serveur', function (r) {
      var requete_sql = 'DELETE FROM messages WHERE id_message = ?';
      var inserts = [r.id_message];
      requete_sql = sql.preparer(mysql, requete_sql, inserts);
      sql.requete(mysql, sql, requete_sql);
      /* la concaténation de 'charger_dernier_message' avec l'id de la discussion
      permet d'éviter que le message se charge chez tous les utilisateurs
      ayant une discussion ouverte */
      socket.emit('suppression_message_client_'+r.id_discussion, r.id_message);
      socket.broadcast.emit('suppression_message_client_'+r.id_discussion,
        r.id_message);
    });

    /* ajoute un "like" à un message tout en vérifiant que l'utilisateur
    n'aie pas déjà aimé le message (chaque utilisateur ne peut aimer qu'une
    seule fois un message) */
    socket.on('like_message', function(d) { // id_message, nom_utilisateur
      if (d.nom_utilisateur !== 'session inexistante') {
        var requete_sql = 'SELECT nom_utilisateur FROM likes WHERE id_message = ?';
        var inserts = [d.id_message];
        requete_sql = sql.preparer(mysql, requete_sql, inserts);
        sql.requete(mysql, sql, requete_sql, function(results) {
          for (var i = 0; i < results.length; i++) {
            if (results[i].nom_utilisateur === d.nom_utilisateur) {
              var deja_like = true;
              return;
            }
          }
          if (!deja_like) {
            var requete_sql = 'SELECT likes FROM messages WHERE id_message = ?';
            requete_sql = sql.preparer(mysql, requete_sql, inserts);
            sql.requete(mysql, sql, requete_sql, function(results) {
              var likes_actuels = results[0].likes + 1;
                var requete_sql = 'UPDATE messages SET likes = ? WHERE id_message = ?';
              var inserts = [likes_actuels, d.id_message];
              requete_sql = sql.preparer(mysql, requete_sql, inserts);
              sql.requete(mysql, sql, requete_sql);

              var requete_sql = 'INSERT INTO likes(id_message, nom_utilisateur) \
                VALUES (?, ??)';
              var inserts = [d.id_message, d.nom_utilisateur];
              requete_sql = sql.preparer(mysql, requete_sql, inserts);
              sql.requete(mysql, sql, requete_sql);

              socket.emit('update_likes'+d.id_discussion, {
                id_message : d.id_message,
                nombre_likes : likes_actuels
              });
              socket.broadcast.emit('update_likes'+d.id_discussion, {
                id_message : d.id_message,
                nombre_likes : likes_actuels
              });
            });
          }
        });
      }
    });

    // change l'avatar d'un utilisateur
    socket.on('changement_avatar', function(d) {
      var requete_sql = 'UPDATE users SET avatar = ? WHERE nom_utilisateur = ??';
      var inserts = [d.avatar, d.nom_utilisateur];
      requete_sql = sql.preparer(mysql, requete_sql, inserts);
      sql.requete(mysql, sql, requete_sql);
    });

    socket.on('nouvelle_discussion', function(discussion) {
      // sélectionne le dernier ID de discussion
      var requete_sql = '\
        SELECT id_discussion FROM messages \
        ORDER BY id_discussion DESC \
        LIMIT 0,1';
      sql.requete(mysql, sql, requete_sql, function(results) {
        /* incrémente le dernier ID de 1,
          ce qui détermine l'ID de la nouvelle discussion */
        var id_nouvelle_discussion = results[0].id_discussion+1;
        // insert le premier message de la discussion
        var requete_sql = '\
          INSERT INTO \
            messages(nom_utilisateur, contenu, date_ecriture, id_discussion, likes)\
          VALUES(??, ??, NOW(), ?, 0)';
        var inserts = [
          discussion.nom_utilisateur, discussion.message, id_nouvelle_discussion
        ];
        requete_sql = sql.preparer(mysql, requete_sql, inserts);
        sql.requete(mysql, sql, requete_sql);
        // attribue le sujet à la discussion dans la table "discussions"
        var requete_sql = '\
          INSERT INTO discussions(sujet, id_discussion)\
          VALUES(??, ?)';
        var inserts = [discussion.sujet.replace(/'/g, "\\'"), id_nouvelle_discussion];
        requete_sql = sql.preparer(mysql, requete_sql, inserts);
        sql.requete(mysql, sql, requete_sql, function() {
          // redirige l'utilisateur vers la nouvelle discussion
          socket.emit('redirection', id_nouvelle_discussion);
          // requête pour charger la dernière discussion dans la page d'accueil
          socket.broadcast.emit('charger_derniere_discussion');
        });
      });
    });
  });
}
./serveur/sql.js
/* crée une connection toujours active entre
le serveur et la base de données */
exports.pool = function(mysql) {
  var pool = mysql.createPool({
    host     : 'localhost',
    user     : 'root',
    password : 'root',
    database : 'forum',
    charset  : 'UTF8_UNICODE_CI',
    multipleStatements : true
  });
  return pool;
}

// exécute une requête SQL
exports.requete = function(mysql, sql, requete_sql, callback) {
  sql.pool(mysql).getConnection(function(err, connection) {
    connection.query(requete_sql, function(err, results) {
      sql.query_error(err);
      if(callback) {
        callback(results);
      }
      connection.destroy();
    });
  });
}

exports.query_error = function(erreur) {
  if (erreur) {
    console.log('query error : ' + erreur.stack);
  }
}

// prépare une requête SQL
exports.preparer = function(mysql, requete_sql, inserts) {
  requete_sql = mysql.format(requete_sql, inserts)
    .replace(/`/g, "'")
    .replace(/'\.'/g, ".");
  return requete_sql;
}

// retourne toutes les informations pour afficher un profil
exports.profil = function(mysql, sql, utilisateur, callback) {
  var requete_sql = 'SELECT * FROM users WHERE nom_utilisateur = ??';
  var inserts = [utilisateur];
  requete_sql = sql.preparer(mysql, requete_sql, inserts);
  sql.requete(mysql, sql, requete_sql, function(results) {

    callback(results[0]);
  });
}

// charge les discussions sur la page d'accueil
exports.chargement_discussions = function(mysql, sql, tout_charger, callback) {
  if (tout_charger) {
    // toutes les discussions sont chargées
    var requete_sql = '\
      SELECT id_discussion\
      FROM discussions\
      ORDER BY id_discussion';
  }
  else {
    // seule la dernière discussion est chargée
    var requete_sql = '\
      SELECT id_discussion\
      FROM discussions\
      ORDER BY id_discussion DESC\
      LIMIT 0,1';
  }

  sql.requete(mysql, sql, requete_sql, function(results) {
    var requete_sql = '';
    for (var i = 0; i < results.length; i++) {
      var requete_temp = '\
        SELECT\
          COUNT(messages.id_message) AS "nombre_messages",\
          discussions.sujet,\
          discussions.id_discussion,\
          messages.nom_utilisateur AS "utilisateur_createur",\
          messages.date_ecriture\
        FROM discussions, messages\
        WHERE discussions.id_discussion = messages.id_discussion\
        AND messages.id_discussion = ?\
        ORDER BY messages.date_ecriture\
        LIMIT 0,1;';
      var inserts = [results[i].id_discussion];
      requete_sql = requete_sql + sql.preparer(mysql, requete_temp, inserts);
    }
    sql.requete(mysql, sql, requete_sql, function(results) {

      if (tout_charger) {
        callback(results);
      } else {
        callback([results]);
      }
    });
  });
}

// retourne le sujet d'une discussion
exports.sujet_discussion = function(mysql, sql, id_discussion, callback) {
  var requete_sql = 'SELECT sujet FROM discussions WHERE id_discussion = ?';
  var inserts = [id_discussion];
  requete_sql = sql.preparer(mysql, requete_sql, inserts);

  sql.requete(mysql, sql, requete_sql, function(results) {
    callback(results[0].sujet);
  });
}

// charge tous ou le dernier des messages d'une discussionl
exports.chargement_messages = function(
  id_discussion,
  mysql,
  sql,
  tout_charger,
  callback) {

    // requête SQL pour vérifier si la discussion existe
    var requete_sql = '\
      SELECT id_discussion\
      FROM discussions\
      WHERE id_discussion = ?';
    var inserts = [id_discussion];
    requete_sql = sql.preparer(mysql, requete_sql, inserts);

    sql.requete(mysql, sql, requete_sql, function(results) {

      // on vérifie si la variable results[0] contient l'id de discussion
      if (!results[0]) {
        // on retourne false à l'argument discussion_existante
        // ceci est ensuite traité par routes.js et envoie une page d'erreur
        callback(false);

      } else {
        // on récupère d'abord le sujet de la discussion
        sql.sujet_discussion(mysql, sql, id_discussion,
          function(sujet_discussion) {

            // une fois qu'on l'a on récupère tous les messages
            var requete_sql = '\
              SELECT\
                messages.id_message AS "id_message",\
                messages.nom_utilisateur AS "nom_utilisateur",\
                messages.contenu AS "contenu",\
                messages.date_ecriture AS "date_ecriture",\
                messages.date_modification AS "date_modification",\
                messages.id_discussion AS "id_discussion",\
                messages.likes AS "likes",\
                users.avatar AS "avatar"\
              FROM users, messages\
              WHERE messages.id_discussion = ?\
              AND messages.nom_utilisateur = users.nom_utilisateur\
              ORDER BY messages.date_ecriture';

            // si on ne veut que le dernier message...
            if (!tout_charger) {
              requete_sql += ' DESC LIMIT 0,1';
            }

            var inserts = [id_discussion];
            requete_sql = sql.preparer(mysql, requete_sql, inserts);

            sql.requete(mysql, sql, requete_sql, function(results) {
              // enfin, on peut envoyer le sujet de discussion et les messages
              callback(true, sujet_discussion, results);
            });
          }
        );
      }
    }
  );
}

Code côté client

./static/js/conversion_dates.js
convertir_date = function(date, id_html) {
  date = new Date(date);
  var jour_mois = date.getDate();
  var jour = date.getDay();
  var mois = date.getMonth();
  var annee = date.getFullYear();
  var heure = date.getHours();
  var minutes = date.getMinutes();

  switch(jour) {
    case 1: jour = 'lundi'; break;
    case 2: jour = 'mardi'; break;
    case 3: jour = 'mercredi'; break;
    case 4: jour = 'jeudi'; break;
    case 5: jour = 'vendredi'; break;
    case 6: jour = 'samedi'; break;
    case 0: jour = 'dimanche'; break;
  }

  switch(mois) {
    case 0:  mois = 'janvier'; break;
    case 1:  mois = 'février'; break;
    case 2:  mois = 'mars'; break;
    case 3:  mois = 'avril'; break;
    case 4:  mois = 'mai'; break;
    case 5:  mois = 'juin'; break;
    case 6:  mois = 'juillet'; break;
    case 7:  mois = 'août'; break;
    case 8:  mois = 'septembre'; break;
    case 9:  mois = 'octobre'; break;
    case 10: mois = 'novembre'; break;
    case 11: mois = 'décembre'; break;
  }

  if(/^[0-9]$/.test(minutes)) {
    date_francais = jour+' '+jour_mois+' '+mois+' '+annee+' à '+heure+'h0'+minutes;
  }
  else {
    date_francais = jour+' '+jour_mois+' '+mois+' '+annee+' à '+heure+'h'+minutes;
  }

  if(typeof(id_html) === 'undefined') {
    return date_francais;
  }
  else {
    byId(id_html).innerHTML = date_francais;
  }
}
./static/js/discussion.js
var id_discussion = window.location.href.replace(/.+\/([0-9]+)/, '$1');

/* fonction permettant de formatter les guillemets simples et doubles
ainsi que les backslash pour éviter un bug lors de la requête SQL */
function echap(chaine) {
  return chaine
    .replace(/"/g, "#g_doubles")
    .replace(/'/g, "#g_simples")
    .replace(/\\/g, "#backslash")
}

// fonction qui réalise l'inverse de la précédente
function unechap(chaine) {
  return chaine
    .replace(/#g_doubles/g, '"')
    .replace(/#g_simples/g, "'")
    .replace(/#backslash/g, "\\");
}

function publier_message(nom_utilisateur) {
  if (tinyMCE.get('emplacement_nouveau_message').getContent() !== '') {
    socket.emit('nouveau_message', {
     nom_utilisateur : nom_utilisateur,
     contenu : echap(tinyMCE.get('emplacement_nouveau_message').getContent()),
     id_discussion : id_discussion
    });
    tinyMCE.get('emplacement_nouveau_message').setContent('');
  }
}

function supprimer_message(id_message) {
  socket.emit('suppression_message_serveur', {
    id_discussion : id_discussion,
    id_message : id_message
  });
}

/* "like" d'un message
à noter que cette fonction envoie également le nom d'utilisateur
pour l'insérer dans la table "likes" de la base de données
et ainsi éviter qu'un utilisateur puisse aimer 2X un message */
function aimer(id_message, nom_utilisateur) {
  socket.emit('like_message', {
    id_message : id_message,
    nom_utilisateur : nom_utilisateur,
    id_discussion : id_discussion
  });
}

/* retourne simplement l'objet javascript de la classe "zone_message"
à partir d'un objet contenant une propriété "id_message" */
function zone_message(classe, d) {
  return byId(d.id_message)
    .getElementsByClassName('zone_message')[0]
    .getElementsByClassName(classe)[0];
}

/* affiche l'éditeur TinyMCE à la place du contenu du message
masque le bouton "modifier" et affiche le bouton "enregistrer" */
function modifier_message(id_message) {
  var d = { id_message : id_message };
  byId(id_message+'_bouton_modifier').style.display = 'none';
  byId(id_message+'_bouton_modifier_mobile').style.display = 'none';
  byId(id_message+'_bouton_enregistrer').style.display = 'block';
  byId(id_message+'_bouton_enregistrer_mobile').style.display = 'inline';
  zone_message('affichage', d).style.display = 'none';
  zone_message('editeur', d).style.display = 'block';
  // récupère l'ID aléatoire de TinyMCE
  var id_tinymce = zone_message('editeur', d).getElementsByTagName('textarea')[0].id;
  init_editeur(id_tinymce);
}

/* enregistre le message, envoie un événement socket, masque le bouton "enregistrer",
affiche le bouton "modifier". A noter que le message est modifié sur la page une
fois que socket.io retourne l'événement "update_message" */
function enregistrer_message(id_message) {
  var d = { id_message : id_message };
  byId(id_message+'_bouton_modifier').style.display = 'block';
  byId(id_message+'_bouton_modifier_mobile').style.display = 'inline';
  byId(id_message+'_bouton_enregistrer').style.display = 'none';
  byId(id_message+'_bouton_enregistrer_mobile').style.display = 'none';
  zone_message('affichage', d).style.display = 'block';
  zone_message('editeur', d).style.display = 'none';
  var id_tinymce = zone_message('editeur', d).getElementsByTagName('textarea')[0].id;
  socket.emit('modification_message', {
    contenu: echap(tinyMCE.get(id_tinymce).getContent()),
    id_message: id_message,
    id_discussion : id_discussion
  });
}

/* télécharge le dernier message à l'aide d'une requête AJAX
s'exécute lorsqu'un utilisateur a publié un message sur la discussion */
socket.on('charger_dernier_message'+id_discussion, function() {
  var xhr = new XMLHttpRequest();
  xhr.open('GET', 'dernier_'+id_discussion);
  xhr.addEventListener('readystatechange', function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
      byId('messages').innerHTML += xhr.responseText;
      var dates_ecriture = byId('messages').getElementsByClassName('date_ecriture');
      var id = dates_ecriture[dates_ecriture.length-1]
        .getElementsByTagName('span')[0].id;
      convertir_date(Date.now(), id);
    }
  });
  xhr.send(null);
});

// supprime un message du côté client
socket.on('suppression_message_client_'+id_discussion, function(id_message) {
  byId(id_message).style.display = 'none';
});

// met à jour les likes d'un message
socket.on('update_likes'+id_discussion, function(m) {
  byId(m.id_message).getElementsByClassName('likes')[0].innerHTML = m.nombre_likes;
  byId(m.id_message).getElementsByClassName('likes')[1].innerHTML = m.nombre_likes;
})

/* met à jour le contenu d'un message, change l'ID de la textarea pour que le
message puisse toujours être modifié */
socket.on('update_message'+id_discussion, function(d) {
  zone_message('date_modification', d).innerHTML = 'modifié le ' +
    convertir_date(Date.now());
  zone_message('editeur', d).innerHTML = '<textarea id="'+Date.now()+'">' +
    unechap(d.contenu)+'</textarea>';
  zone_message('affichage', d).innerHTML = unechap(d.contenu);
});
./static/js/general.js
var socket = io.connect('http://' + window.location.host);

function byId(id) {
  return document.getElementById(id);
}

function byClass(classe) {
  return document.getElementsByClassName(classe);
}

function init_editeur(id_editeur) {
  var tinymce_toolbar = "undo redo | \
  n eqneditor | \
  n underline bold italic | \
  n alignleft aligncenter alignright alignjustify | \
  n bullist numlist outdent indent | \
  n image link code | \
  n forecolor backcolor emoticons";
  var tinymce_plugins = "eqneditor textcolor image code emoticons";
  tinyMCE.init({
      selector: '#'+id_editeur,
      plugins: tinymce_plugins,
      toolbar: tinymce_toolbar,
      language: "fr_FR"
  });
}

function navbar_mobile() {
  if (byId('contenu_nav').className === 'navbar-collapse collapse') {
    byId('contenu_nav').className = 'navbar-collapse collapse in';
    byId('navbar').style.height = '100%';
  } else {
    byId('contenu_nav').className = 'navbar-collapse collapse';
    byId('navbar').style.height = '50px';
  }
}
./static/js/index.js
socket.on('charger_derniere_discussion', function() {
  var xhr = new XMLHttpRequest();
  xhr.open('GET', 'derniere_discussion');
  xhr.addEventListener('readystatechange', function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
      byId('retour_ajax').innerHTML += xhr.responseText;
    }
  });
  xhr.send(null);
});

function rechercher() {
  byId('chargement_discussions').innerHTML = '';
  byId('retour_ajax').innerHTML = '';
  var recherche = byId('recherche_discussion').value;
    var discussions = byClass('discussion');
    for (var i = 0; i < discussions.length; i++) {
      discussions[i].style.display = 'none';
    }
    for (var i = 0; i < discussions.length; i++) {
      var sujet = discussions[i].getElementsByClassName('sujet_texte')[0].innerHTML;
      var regex = new RegExp(recherche, 'i');
      if (regex.test(sujet)) {
        discussions[i].style.display = 'block';
      }
    }
}
./static/js/login.js
// cette fonction envoie l'événement socket "login"
function login() {
  socket.emit('login', {
    nom_utilisateur : byId('login_utilisateur').value,
    password : byId('login_password').value
  });
}

function verifier_enter(event, form, callback) {
  // fonction tirée de http://stackoverflow.com/questions/14251676/
  var code = (event.keyCode ? event.keyCode : event.which);
  if(code === 13) {
    callback();
  }
}

// met les zones de texte en rouge et affiche le message d'erreur
socket.on('erreur_login', function() {
  byId('form_login_utilisateur').className += ' has-error';
  byId('form_login_password').className += ' has-error';
  byId('login_incorrect').style.display = 'inline';
});

// redirige l'utilisateur vers l'URL indiquée dans l'événement socket
// (en l'occurence, la page "token")
socket.on('redirection', function(url) {
  window.location.assign(url);
});
./static/js/new.js
// publie un message depuis une nouvelle discussion
function publier_message(nom_utilisateur) {
  if (byId('input_sujet').value !== '' &&
    tinyMCE.get('emplacement_nouveau_message').getContent() !== '') {
      socket.emit('nouvelle_discussion', {
        sujet : byId('input_sujet').value,
        message : tinyMCE.get('emplacement_nouveau_message').getContent(),
        nom_utilisateur : nom_utilisateur
    });
  }
}

// redirige l'utilisateur vers la nouvelle discussion
socket.on('redirection', function(id_discussion) {
  window.location.assign(id_discussion);
});
./static/js/nouveau_compte.js
// crée un nouveau compte
function nouveau_compte() {
  if (verifier('utilisateur') &&
    verifier('prenom') &&
    verifier('nom') &&
    verifier('mail') &&
    verifier('password'))
  {

    socket.emit('nouveau_compte', {
      nom_utilisateur : byId('nc_utilisateur').value,
      password : byId('nc_password').value,
      prenom : byId('nc_prenom').value,
      nom : byId('nc_nom').value,
      mail : byId('nc_mail').value
    });
  }
}

// affiche un message d'erreur si l'utilisateur existe
socket.on('utilisateur_existant', function() {
  byId('utilisateur_existant').style.display = 'block';
});
./static/js/profil.js
/* change de classe bootstrap la balise <div> contenant
les informations sur l'utilisateur pour que l'avatar puisse
pendre toute la largeur de l'écran */
function responsive() {
  if (window.matchMedia("(max-width: 400px)").matches) {
    byId('infos_profil').className = 'col-xs-12';
  }
  else {
    byId('infos_profil').className = 'col-xs-8';
  }

  if (!window.matchMedia("(min-width: 399px)").matches) {
    byId('infos_profil').className = 'col-xs-12';
  }
  else {
    byId('infos_profil').className = 'col-xs-8';
  }
}

// vérifie le formulaire de modification de données
// entoure les champs correspondants en vert
function verifier_form() {
  verifier('prenom');
  verifier('nom');
  verifier('mail');
}

// affiche le formulaire de modification des coordonnées
function afficher_mod() {
  byId('mod_password').style.display = 'none';
  byId('mod_avatar').style.display = 'none';
  if (byId('mod_coordonnees').style.display === 'none') {
    byId('mod_coordonnees').style.display = 'block';
  }
  else {
    byId('mod_coordonnees').style.display = 'none';
  }
}

// affiche le formulaire de modification du mot de passe
function afficher_mod_password() {
  byId('mod_coordonnees').style.display = 'none';
  byId('mod_avatar').style.display = 'none';
  if (byId('mod_password').style.display === 'none') {
    byId('mod_password').style.display = 'block';
  }
  else {
    byId('mod_password').style.display = 'none';
  }
}

// affiche le choix d'avatars
function afficher_mod_avatar() {
  byId('mod_password').style.display = 'none';
  byId('mod_coordonnees').style.display = 'none';
  if (byId('mod_avatar').style.display === 'none') {
    byId('mod_avatar').style.display = 'block';
  }
  else {
    byId('mod_avatar').style.display = 'none';
  }
}

// envoie un événement socket pour modifier les coordonnées
function modifier_compte(utilisateur) {
  if (verifier('prenom') &&
    verifier('nom') &&
    verifier('mail'))
  {
    socket.emit('modifier_compte', {
      utilisateur : utilisateur,
      prenom : byId('nc_prenom').value,
      nom : byId('nc_nom').value,
      mail : byId('nc_mail').value,
    });
  }
}

// envoie un événement socket pour modifier le mot de passe
function modifier_password(utilisateur) {
  if (byId('nc_ancien_password').value !== '' && verifier('nouveau_password')) {
    socket.emit('modifier_password', {
      nom_utilisateur : utilisateur,
      ancien_password : byId('nc_ancien_password').value,
      nouveau_password : byId('nc_nouveau_password').value
    });
  }
}

// télécharge les petits avatars et les affiche sur la page
function avatars_small(nom_utilisateur) {
  for (var i = 1; i < 101; i++) {
    byId('mod_avatar').innerHTML +=
      '<image src="static/avatars_petits/'+i+
      '.jpg" onclick="changer_avatar('+i+', \''+nom_utilisateur+'\')">';
  }
}

// change l'avatar d'un utilisateur sur la page et envoie un événement socket
function changer_avatar(n, nom_utilisateur) {
  byClass('avatar_profil_desktop')[0].innerHTML =
    '<image src="static/avatars_grands/'+n+'.jpg" class="avatar_profil"></image>';
  byClass('avatar_profil_mobile')[0].innerHTML =
    '<image src="static/avatars_grands/'+n+'.jpg" class="avatar_profil"></image>';
  socket.emit('changement_avatar', {
    avatar : n,
    nom_utilisateur : nom_utilisateur
  });
}

// affiche un message d'erreur - mot de passe incorrect
socket.on('ancien_password_incorrect', function() {
  byId('form_nc_ancien_password').className = 'form-group has-error';
  byId('ancien_password_incorrect').style.display = 'inline';
});

/* réinitialise les champs des mots de passe et affiche un message confirmant
la modification du mot de passe */
socket.on('mod_password_ok', function() {
  byId('form_nc_ancien_password').className = 'form-group';
  byId('nc_ancien_password').value = '';
  byId('nc_nouveau_password').value = '';
  byId('ancien_password_incorrect').style.display = 'none';
  byId('mod_password').style.display = 'none';
  verifier('nouveau_password');
  byId('password_success').style.display = 'block';
  byId('coordonnees_success').style.display = 'none';
});

/* actualise les nouvelles coordonnées et affiche un message confirmant
la modification des coordonnées chez l'utilisateur qui les a modifiées */
socket.on('mod_compte_ok', function(nouveau) {
    byId('info_prenom').innerHTML = 'Prénom : ' + nouveau.prenom;
    byId('info_nom').innerHTML = 'Nom : ' + nouveau.nom;
    byId('info_email').innerHTML = 'E-mail : ' + nouveau.mail;
    if (!nouveau.broadcast) {
      byId('nc_prenom').value = nouveau.prenom;
      byId('nc_nom').value = nouveau.nom;
      byId('nc_prenom_mobile').value = nouveau.prenom;
      byId('nc_nom_mobile').value = nouveau.nom;
      byId('nc_mail').value = nouveau.mail;
      byId('mod_coordonnees').style.display = 'none';
      byId('password_success').style.display = 'none';
    byId('coordonnees_success').style.display = 'block';
  }
});
./static/js/verification_form.js
function get_regex(data) {
  switch(data) {
    case 'utilisateur':
      return /^[a-z0-9]{4,10}$/i;
    case 'password': case 'nouveau_password':
      return /^.{4,20}$/i;
    case 'prenom': case 'prenom_mobile': case 'nom': case 'nom_mobile':
      return /^[a-zâäàéèùêëîïôöçñ\ ]{2,50}$/i;
    case 'mail':
      return /^[a-z0-9\.-_]+\@[a-z0-9\.-_]+\.[a-z]{2,10}$/i;
  }
}

function get_html_class(data, type) {
  if (data === 'prenom' || data === 'nom') {
    switch(type) {
      case 'success':
        return 'has-success';
      case 'nothing':
        return '';
      case 'error':
        return 'has-error';
    }
  } else {
    switch(type) {
      case 'success':
        return 'form-group has-success';
      case 'nothing':
        return 'form-group';
      case 'error':
        return 'form-group has-error';
    }
  }
}

function verifier(data, stop) {
  var return_value;
  if (get_regex(data).test(byId('nc_'+data).value)) {
    byId('form_nc_'+data).className = get_html_class(data, 'success');
    byId(data+'_incorrect').style.display = 'none';
    return_value = true;
  } else if (byId('nc_'+data).value === '') {
    byId('form_nc_'+data).className = get_html_class(data, 'nothing');
    byId(data+'_incorrect').style.display = 'none';
    return_value = false;
  } else {
    byId('form_nc_'+data).className = get_html_class(data, 'error');
    byId(data+'_incorrect').style.display = 'inline';
    return_value = false;
  }
  if ((data === 'prenom' || data == 'nom') && !stop) {
    byId('nc_'+data+'_mobile').value = byId('nc_'+data).value;
    verifier(data+'_mobile', true);
  }
  if ((data === 'prenom_mobile' || data == 'nom_mobile') && !stop) {
    byId('nc_'+data.replace(/_mobile/, '')).value = byId('nc_'+data).value;
    verifier(data.replace(/_mobile/, ''), true);
  }
  return return_value;
}
./static/responsive.css
@media(max-width:991px) {
  .discussions {
    padding-bottom: 15px;
  }
}
@media(max-width:767px) {
  .desktop {
    display: none;
  }
  .avatar {
    display: none;
  }
  .ligne {
    position:relative;
      bottom: 10px;
  }
  .sujet_discussion {
    margin-bottom: 15px;
  }
  .nom_utilisateur {
    position: relative;
      bottom: 4px;
      right: 3px;
  }
  .editeur {
    margin-right: 10px;
  }
  .form-group {
    padding-left: 17px;
  }
  .ligne {
    margin-bottom: -8px;
  }
}
@media(min-width: 768px) {
  .mobile {
    display: none;
  }
  .nom_utilisateur {
    display: block;
    text-align: center;
  }
  .ligne {
    margin-bottom: 24px;
  }
}

/*avatar du profil*/
.avatar_profil_mobile {
  display: none;
}
@media(max-width: 572px) {
  .avatar_profil {
    max-width: 110%;
      vertical-align: middle;
  }
}

@media(min-width: 570px) {
  #mod_avatar {
    position: relative;
      bottom: 25px;
  }
}
@media(max-width: 500px) {
  .avatar_profil {
    position: relative;
    top: 5px;
  }
}
@media(max-width: 400px) {
  .avatar_profil_desktop {
    display: none;
  }
  .avatar_profil_mobile {
    display: block;
    position: relative;
    bottom: 35px;
    text-align: center;
  }
}
./static/style.css
body {
  background-color: rgb(245, 245, 245);
}
.blanc {
  background-color: rgb(255, 255, 255);
  margin-top: -4px;
  padding-top: 4px;
}
table {
  position: relative;
    top: -2px;
}
.sujet {
  padding-top: 10px;
  padding-bottom: 10px;
}
.sujet_texte {
  font-size: 18px;
  font-weight: bold;
  position: relative;
    top: -1px;
  color:black;
}
.sujet_texte:hover {
  text-decoration: underline;
  color:black;
}
.infos_sujet {
  position: relative;
    top: 2px;
}
.nombre_messages {
  position: relative;
    top: 2px;
  font-weight: bold;
}
.dm {
  color: rgb(150,150,150);
}
.td_message {
  padding-left: 10px;
  padding-right: 10px;
}
.act_message {
  vertical-align: text-top;
}
.btn {
  margin-bottom: 10px;
}
.ecrire_message {
  padding-top: 10px;
}
.help-block {
  position: relative;
    top: -8px;
}
.row {
  position: relative;
    top: -15px;
}
.modification_message {
    min-height: 60px;
    overflow-y: auto;
    word-wrap:break-word;
}
.publication_message {
    float:right;
    position:relative;
        top:5px;
}
.dates {
  color: grey;
  position: relative;
    top: -3px;
}
#input_sujet {
  width: 100%;
  font-size: 20px;
  margin-top: 10px;
  text-align: center;
}
.zone_message {
  max-height: 400px;
  overflow: auto;
  margin-right: -11px;
}
.message_erreur {
  position: relative;
  top: 2px;
  margin-bottom: -15px;
  display: none;
}
.gros_titre {
  font-size:30px;
  text-decoration:bold;
  text-align:center;
  margin-bottom: 25px;
}
.sujet_discussion {
  font-size:30px;
  text-decoration:bold;
  text-align:center;
  margin-bottom: 12px;
  position: relative;
    bottom: 5px;
}
.avatar {
  max-width: 80%;
  display: block;
  margin-left: auto;
  margin-right: auto;
}
.avatar_profil {
  max-width:180px;
  margin-top: 5px;
  position: relative;
  bottom: 20px;
}
.ligne {
  border-top: solid 3px rgb(235,235,235);
  margin-left: -15px;
  margin-right: -15px;
}
.btn-nouveau_compte {
  margin-bottom:4px
  margin-top:-15px;
  position: relative;
    bottom: 9px;
}
#mod_coordonnees, #mod_password {
  padding-right:20px;
  display:none;
}
.editeur {
  margin-top: 10px;
}

HTML et EJS

./views/balise_head.ejs
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">

<title>Forum tm 15-16</title>

<link rel="stylesheet" href="static/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="static/style.css">
<link rel="stylesheet" href="static/responsive.css">

<script src="/socket.io/socket.io.js"></script>
<script src="static/tinymce/tinymce.min.js"></script>

<script src="static/js/general.js"></script>

<!--[if lt IE 9]>
    <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
    <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
./views/barre_navigation.ejs
<nav class="navbar navbar-default" id="navbar" style="height:50px;">
  <div class="navbar-header">
    <a class="navbar-brand" href="/">Forum</a>
      <a href="#"
        class="navbar-toggle"
        onclick="navbar_mobile();">
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </a>
  </div>
  <div class="navbar-collapse collapse" id="contenu_nav">
    <ul class="nav navbar-nav">
      <li><a href="/">Accueil</a></li>
    </ul>
    <% if (nom_utilisateur !== 'session inexistante') { %>
      <form class="navbar-form navbar-left">
        <a href="new" class="btn btn-default">
          <span class="glyphicon glyphicon-pencil"></span>
          Nouvelle discussion
        </a>
      </form>
    <% } %>
    <ul class="nav navbar-nav navbar-right" style="padding-right:15px;">
      <li>
        <% if (nom_utilisateur !== 'session inexistante') { %>
          <li>
            <a href="profil_<%= nom_utilisateur %>" style="color:black;">
              <%= nom_utilisateur %>
            </a>
          </li>
        <% } %>
        <% if (nom_utilisateur === 'session inexistante') { %>
          <a href="nouveau_compte">
            Créer un compte
          </a>
        <% } %>
      <li>
        <% if (nom_utilisateur === 'session inexistante') { %>
          <a href="login">
            Login
          </a>
        <% } else { %>
          <a href="logout">
            Logout
          </a>
        <% } %>
      </li>
    </ul>
  </div>
</nav>
./views/chargement_discussions.ejs
<% convertir_date = function(date) {
  var id_temp = Math.random().toString(36).substring(7); %>
  <span id="<%= id_temp %>"></span>
  <script>convertir_date("<%= date %>","<%= id_temp %>");</script>
<% }
for (var i = 0; i < discussions.length; i++) { %>
  <div class="sujet discussion" id="<%= discussions[i][0].id_discussion %>">
    <table>
      <tr>
        <td style="width:50px;">
          <img src="static/avatar.png" style="max-width:75%;">
        </td>
        <td>
          <a href="<%= discussions[i][0].id_discussion %>" class="sujet_texte">
            <%= discussions[i][0].sujet %>
          </a><br>
          <span class="infos_sujet">
            créé par <a href="profil_<%= discussions[i][0].utilisateur_createur %>">
              <%= discussions[i][0].utilisateur_createur %>
            </a> le
              <% convertir_date(discussions[i][0].date_ecriture); %>
          </span>
          <span class="nombre_messages">
            <%= discussions[i][0].nombre_messages %>
            <% if (discussions[i][0].nombre_messages === 1) { %> message <% } %>
            <% if (discussions[i][0].nombre_messages > 1) { %> messages <% } %>
          </span>
        </td>
      </tr>
    </table>
  </div>
<% } %>
./views/chargement_messages.ejs
<!-- cette fonction sert à lancer la conversion d'une date formatée par mysql.
elle utilise le fichier conversion_dates.js -->
<% convertir_date = function(date) {
  var id_temp = Math.random().toString(36).substring(7); %>
  <span id="<%= id_temp %>"></span>
  <script>convertir_date("<%= date %>","<%= id_temp %>");</script>
<% }
<!-- cette boucle parcourt tous les messages et applique à chacun un formattage -->
for (var i = 0; i < m.length; i++) { %>
  <!-- les lignes suivantes traitent quelques caractères spéciaux (' " \)
  qui créent des bugs s'ils ne sont pas "encodés" -->
  <% m[i].contenu = m[i].contenu
    .replace(/#g_doubles/g, '"')
    .replace(/#g_simples/g, "'")
    .replace(/#backslash/g, "\\"); %>
  <!-- cet attribut ID permet de sélectionner plus facilement
  et supprimer les messages -->
  <div id="<%= m[i].id_message %>">
    <p class="ligne"></p>
    <div class="row">
      <div class="col-sm-2 zone_utilisateur">
        <image src="static/avatars_grands/<%= m[i].avatar %>.jpg" class="avatar"><br>
      <div>
        <a href="profil_<%= m[i].nom_utilisateur %>" class="nom_utilisateur">
          <%= m[i].nom_utilisateur %>
        </a>
        <!-- affichage sur les appareils dont la largeur de la fenêtre est petite -->
        <span class="mobile">
          <a onclick="aimer(<%= m[i].id_message %>, '<%= nom_utilisateur %>')"
            class="btn btn-sm btn-success">
              <span class="glyphicon glyphicon-heart"></span>
              <span class="likes">
                <%= m[i].likes %>
              </span>
          </a>
          <!-- les boutons suivants s'affichent seulement si l'utilisateur connecté
          a écrit le message -->
          <% if (nom_utilisateur === m[i].nom_utilisateur) { %>
            <!-- bouton "modifier" -->
            <span id="<%= m[i].id_message %>_bouton_modifier_mobile">
              <span onclick="modifier_message(<%= m[i].id_message %>)"
                class="btn btn-sm btn-default">
                <span class="glyphicon glyphicon-pencil"></span>
                  modifier
              </span>
            </span>
            <!-- bouton "enregistrer", qui s'affichera après un clic sur "modifier" -->
            <span id="<%= m[i].id_message %>_bouton_enregistrer_mobile"
            style="display: none;">
              <span onclick="enregistrer_message(<%= m[i].id_message %>)"
                class="btn btn-sm btn-default">
                <span class="glyphicon glyphicon-pencil"></span>
                  enregistrer
              </span>
            </span>
            <span>
            <!-- bouton "supprimer" -->
              <span onclick="supprimer_message(<%= m[i].id_message %>)"
                class="btn btn-sm btn-danger">
                <span class="glyphicon glyphicon-remove"></span>
                  supprimer
              </span><br>
            </span>
          <% } %>
        </span>
      </div>
        <center>
          <!-- affichage sur les appareils dont la largeur de la fenêtre
          est plus grande -->
          <span class="desktop">
            <a onclick="aimer(<%= m[i].id_message %>, '<%= nom_utilisateur %>')"
            class="btn btn-sm btn-success"
            style="margin-top: 5px;">
              <span class="glyphicon glyphicon-heart"></span>
              <span class="likes">
                <%= m[i].likes %>
              </span>
            </a><br>
          </span>
        </center>
      </div>
      <!-- si les boutons s'affichent, la balise prend la classe bootstrap col-sm-8 -->
      <% if (nom_utilisateur === m[i].nom_utilisateur) { %>
        <div class="td_message col-sm-8">
      <% } else { %>
      <!-- dnas le cas contraire, on peut se permettre
      d'avoir une zone de texte plus grande -->
        <div class="td_message col-sm-10">
      <% } %>
        <div class="zone_message">
          <div class="dates">
            <span class="date_ecriture">
              écrit le <% convertir_date(m[i].date_ecriture); %><br>
            </span>
            <span class="date_modification">
              <% if (m[i].date_modification !== '0000-00-00 00:00:00') { %>
                modifié le <% convertir_date(m[i].date_modification); %><br>
              <% } %>
            </span>
          </div>
          <div class="editeur mod_message" style="display: none;">
            <!-- la textarea contient un attribut ID généré au hasard
            pour permettre à TinyMCE de générer l'éditeur plusieurs fois
            (nous verrons ceci lorsque nous nous occuperons d'afficher TinyMCE) -->
            <textarea
            id="<%= m[i].id_message + Math.random().toString(36).substring(7); %>">
              <%= m[i].contenu %>
            </textarea>
          </div>
          <span class="affichage">
            <%- m[i].contenu %>
          </span>
        </div>
      </div>
      <% if (nom_utilisateur === m[i].nom_utilisateur) { %>
        <!-- affichage des boutons sur les fenêtres plus larges -->
        <div class="desktop">
          <!-- l'attribut "style" force le texte à s'afficher en haut -->
          <div class="col-sm-2" style="vertical-align: text-top;">
            <center>
              <!-- bouton "modifier" -->
              <div id="<%= m[i].id_message %>_bouton_modifier">
                <span onclick="modifier_message(<%= m[i].id_message %>)"
                  class="btn btn-sm btn-default">
                  <span class="glyphicon glyphicon-pencil"></span>
                    modifier
                </span><br>
              </div>
            </center>
            <center>
              <!-- bouton "enregistrer" -->
              <div id="<%= m[i].id_message %>_bouton_enregistrer"
              style="display: none;">
                <span onclick="enregistrer_message(<%= m[i].id_message %>)"
                  class="btn btn-sm btn-default">
                  <span class="glyphicon glyphicon-pencil"></span>
                    enregistrer
                </span><br>
              </div>
            </center>
            <center>
              <div>
                <!-- bouton "supprimer" -->
                <span onclick="supprimer_message(<%= m[i].id_message %>)"
                  class="btn btn-sm btn-danger">
                  <span class="glyphicon glyphicon-remove"></span>
                    supprimer
                </span><br>
              </div>
            </center>
          </div>
        </div>
      <% } %>
    </div>
  </div>
<% } %>
./views/discussion.ejs
<!DOCTYPE html>
<html lang="fr">
  <head>
    <% include balise_head %>
    <script src="static/js/conversion_dates.js"></script>
    <script src="static/js/discussion.js"></script>
  </head>
  <body>
    <% include barre_navigation %>
    <div class="container">
      <div class="row">
        <div class="col-lg-offset-2 col-md-offset-1 col-md-10 col-lg-8
        blanc discussions">
          <p>
            <% if (nom_utilisateur === 'session inexistante') { %>
              <div class="alert alert-info" style="text-align:justify">
                Vous n'êtes actuellement pas connecté.
                Par conséquent, vous ne pouvez pas aimer ou publier des messages.
                <a href="login">
                  Connectez-vous
                </a>
                pour participer à la discussion.
              </div>
             <% } %>
          </p>
          <div class="sujet_discussion">
            <%= sujet_discussion %>
          </div>
          <div id="messages">
            <% include chargement_messages %>
          </div>
          <% if (nom_utilisateur !== 'session inexistante') { %>
            <% include editeur %>
          <% } %>
        </div>
        <div class="col-lg-offset-2 col-md-offset-1"></div>
      </div>
    </div>
  </body>
<html>
./views/editeur.ejs
<p class="editeur">
  <textarea id="emplacement_nouveau_message" class="form-control">
  </textarea>
    <script>
      init_editeur('emplacement_nouveau_message');
    </script>
  <div class="help-block">Vous pouvez agrandir la fenêtre
    <span onclick="publier_message('<%= nom_utilisateur %>');"
      class="btn btn-primary publication_message" >
      Publier
    </span>
  </div>
</p>
./views/erreur_discussion.ejs
Cette discussion n'existe pas
./views/error.ejs
Cette page n'existe pas
./views/form_nouveau_compte.ejs
<div class="row">
  <div class="form-group" id="form_nc_utilisateur">
    <label for="nc_utilisateur" class="col-lg-3 col-sm-3 control-label">
      Nom d'utilisateur
    </label>
    <div class="col-lg-9 col-sm-9">
      <input type="text" class="form-control" id="nc_utilisateur"
      onkeyup="verifier('utilisateur');">
      <span class="help-block message_erreur" id="utilisateur_incorrect">
        Le nom d'utilisateur doit contenir au minimum 4 caractères,
        au maximum 10 caractères, et peut être uniquement composé de chiffres
        et de lettres non-accentuées.
      </span>
    </div>
  </div>
</div>
<div class="row desktop">
  <div class="form-group">
    <div id="form_nc_prenom">
     <label for="nc_prenom" class="col-sm-3 control-label">Prénom</label>
     <div class="col-lg-4 col-sm-4">
       <input type="text" class="form-control" id="nc_prenom"
       onkeyup="verifier('prenom');">
       <span class="help-block message_erreur" id="prenom_incorrect">
          Ce prénom est invalide.
       </span>
     </div>
    </div>
    <div id="form_nc_nom">
      <label for="nc_nom" class="col-sm-1 control-label">Nom</label>
      <div class="col-lg-4 col-sm-4">
        <input type="text" class="form-control" id="nc_nom"
        onkeyup="verifier('nom');">
        <span class="help-block message_erreur" id="nom_incorrect">
          Ce nom est invalide.
        </span>
      </div>
    </div>
  </div>
</div>
<div class="row mobile">
  <div class="form-group" id="form_nc_prenom_mobile">
    <label for="nc_prenom_mobile" class="col-sm-4 control-label">
      Prénom
    </label>
    <div class="col-lg-8 col-sm-8">
      <input type="text" class="form-control" id="nc_prenom_mobile"
      onkeyup="verifier('prenom_mobile');">
      <span class="help-block message_erreur" id="prenom_mobile_incorrect">
        Ce prénom est invalide.
      </span>
    </div>
  </div>
</div>
<div class="row mobile">
  <div class="form-group" id="form_nc_nom_mobile">
    <label for="nc_nom_mobile" class="col-sm-4 control-label">Nom</label>
    <div class="col-lg-8 col-sm-8">
      <input type="text" class="form-control" id="nc_nom_mobile"
      onkeyup="verifier('nom_mobile');">
      <span class="help-block message_erreur" id="nom_mobile_incorrect">
        Ce nom est invalide.
      </span>
    </div>
  </div>
</div>
<div class="row">
  <div class="form-group" id="form_nc_mail">
    <label for="nc_mail" class="col-lg-2 col-sm-2 control-label">Mail</label>
    <div class="col-lg-10 col-sm-10">
      <input type="text" class="form-control" id="nc_mail"
      onkeyup="verifier('mail');">
      <span class="help-block message_erreur" id="mail_incorrect">
        Cette adresse e-mail est invalide.
      </span>
    </div>
  </div>
</div>
<div class="row">
  <div class="form-group" id="form_nc_password">
    <label for="nc_password" class="col-lg-3 col-sm-3 control-label">
      Mot de passe
    </label>
    <div class="col-lg-9 col-sm-9">
      <input type="password" class="form-control" id="nc_password"
      onkeypress="verifier_enter(event, this, nouveau_compte);"
      onkeyup="verifier('password');">
      <span class="help-block message_erreur" id="password_incorrect">
        Le mot de passe doit contenir au minimum 4 caractères,
        au maximum 20 caractères, et peut être uniquement
        composé de chiffres et de lettres.
      </span>
    </div>
  </div>
</div>
./views/form_profil.ejs
<form class="form-horizontal" id="mod_coordonnees">
  <div class="form-group">
    <h4 class="gros_titre">Modifier les coordonnées</h4>
  </div>
  <div class="row desktop">
    <div class="form-group">
      <div id="form_nc_prenom">
       <label for="nc_prenom" class="col-sm-3 control-label">Prénom</label>
       <div class="col-lg-4 col-sm-4">
         <input type="text" class="form-control" id="nc_prenom"
         onkeyup="verifier('prenom');">
         <span class="help-block message_erreur" id="prenom_incorrect">
            Ce prénom est invalide.
         </span>
       </div>
      </div>
      <div id="form_nc_nom">
        <label for="nc_nom" class="col-sm-1 control-label">Nom</label>
        <div class="col-lg-4 col-sm-4">
          <input type="text" class="form-control" id="nc_nom"
          onkeyup="verifier('nom');">
          <span class="help-block message_erreur" id="nom_incorrect">
            Ce nom est invalide.
          </span>
        </div>
      </div>
    </div>
  </div>
  <div class="row mobile">
    <div class="form-group" id="form_nc_prenom_mobile">
      <label for="nc_prenom_mobile" class="col-sm-4 control-label">Prénom</label>
      <div class="col-lg-8 col-sm-8">
        <input type="text" class="form-control" id="nc_prenom_mobile"
        onkeyup="verifier('prenom_mobile');">
        <span class="help-block message_erreur" id="prenom_mobile_incorrect">
          Ce prénom est invalide.
        </span>
      </div>
    </div>
  </div>
  <div class="row mobile">
    <div class="form-group" id="form_nc_nom_mobile">
      <label for="nc_nom_mobile" class="col-sm-4 control-label">Nom</label>
      <div class="col-lg-8 col-sm-8">
        <input type="text" class="form-control" id="nc_nom_mobile"
        onkeyup="verifier('nom_mobile');">
        <span class="help-block message_erreur" id="nom_mobile_incorrect">
          Ce nom est invalide.
        </span>
      </div>
    </div>
  </div>
  <div class="row">
    <div class="form-group" id="form_nc_mail">
      <label for="nc_mail" class="col-lg-3 col-sm-3 control-label">Mail</label>
      <div class="col-lg-9 col-sm-9">
        <input type="text" class="form-control" id="nc_mail"
        onkeyup="verifier('mail');">
        <span class="help-block message_erreur" id="mail_incorrect">
          Cette adresse e-mail est invalide.
        </span>
      </div>
    </div>
  </div>
  <div class="form-group">
    <span class="pull-right btn btn-primary btn-nouveau_compte"
    onclick="modifier_compte('<%= profil.nom_utilisateur %>');">
      Enregistrer
    </span>
  </div>
</form>
<form class="form-horizontal" id="mod_password">
  <div class="form-group">
    <h4 class="gros_titre">Modifier le mot de passe</h4>
  </div>
  <div class="row">
    <div class="form-group" id="form_nc_ancien_password">
      <label for="form_nc_ancien_password" class="col-lg-3 col-sm-4 control-label">
        Ancien mot de passe
      </label>
      <div class="col-lg-9 col-sm-8">
        <input type="password" class="form-control" id="nc_ancien_password">
        <span class="help-block message_erreur" id="ancien_password_incorrect">
          Le mot de passe est incorrect.
        </span>
      </div>
    </div>
  </div>
  <div class="row">
    <div class="form-group" id="form_nc_nouveau_password">
      <label for="form_nc_nouveau_password" class="col-lg-3 col-sm-4 control-label">
        Nouveau mot de passe
      </label>
      <div class="col-lg-9 col-sm-8">
        <input type="password" class="form-control" id="nc_nouveau_password"
        onkeyup="verifier_nouveau_password();">
        <span class="help-block message_erreur" id="nouveau_password_incorrect">
          Le mot de passe doit contenir au minimum 4 caractères,
          au maximum 20 caractères, et peut être uniquement
          composé de chiffres et de lettres.
        </span>
      </div>
    </div>
  </div>
  <div class="form-group">
    <span class="pull-right btn btn-primary btn-nouveau_compte"
    onclick="modifier_password('<%= profil.nom_utilisateur %>');">
      Enregistrer
    </span>
  </div>
</form>
./views/index.ejs
<!DOCTYPE html>
<html>
  <head>
    <% include balise_head %>
    <script src="static/js/index.js"></script>
    <script src="static/js/conversion_dates.js"></script>
  </head>
  <body>
    <% include barre_navigation %>
    <div class="container">
      <div class="row">
        <div class="col-lg-offset-2 col-md-offset-1 col-md-10 col-lg-8 blanc">
          <p style="padding-top:6px;">
            <input
              id="recherche_discussion"
              placeholder="rechercher une discussion"
              onkeyup="rechercher();"
              class="form-control">
            </input>
          </p>
          <p>
            <span id="chargement_discussions">
              <% include chargement_discussions %>
            </span>
            <span id="retour_ajax"></span>
          </p>
        <div class="col-lg-offset-2 col-md-offset-1"></div>
      </div>
    </div>
  </body>
</html>
./views/login.ejs
<!DOCTYPE html>
<html lang="fr">
  <head>
    <% include balise_head %>
    <script src="static/js/login.js"></script>
  </head>

  <body>
    <% include barre_navigation %>

    <div class="container">
      <div class="row">
        <div class="col-lg-offset-2 col-md-offset-1 col-md-10 col-lg-8
        blanc corps_page">
          <center>
            <div class="alert alert-info">
                Vous n'avez pas encore de compte ?
                <a href="nouveau_compte">Inscrivez-vous</a> maintenant !
            </div>
          </center>
          <form class="form-horizontal" style="padding-right:20px;">
            <div class="form-group">
              <h4 class="gros_titre">Se connecter</h4>
            </div>
            <div class="row">
              <div class="form-group" id="form_login_utilisateur">
                <label for="login_utilisateur"
                class="col-lg-3 col-sm-4 control-label">
                  Nom d'utilisateur
                </label>
                <div class="col-lg-9 col-sm-8">
                  <input type="text" class="form-control" id="login_utilisateur">
                  <span class="help-block message_erreur"
                    id="login_incorrect"
                    style="position: relative; top: 6px;">
                      Le nom d'utilisateur ou le mot de passe est incorrect.
                  </span>
                </div>
              </div>
            </div>
            <div class="row">
              <div class="form-group" id="form_login_password">
                <label for="login_password" class="col-lg-3 col-sm-4 control-label">
                  Mot de passe
                </label>
                <div class="col-lg-9 col-sm-8">
                  <input type="password"
                    class="form-control"
                    id="login_password"
                    onkeypress="verifier_enter(event, this, login);">
                </div>
              </div>
            </div>
            <div class="form-group">
              <span
                class="pull-right btn btn-primary"
                style="margin-top: -8px; margin-bottom: 16px;"
                onclick="login();">
                  Login
              </span>
            </div>
          </form>
        <div class="col-lg-offset-2 col-md-offset-1"></div>
      </div>
    </div>
  </body>
</html>
./views/new.ejs
<!DOCTYPE html>
<html lang="fr">
  <head>
    <% include balise_head %>
    <script src="static/js/new.js"></script>
  </head>
  <body>
    <% include barre_navigation %>
    <div class="container">
      <div class="row">
        <div class="col-lg-offset-2 col-md-offset-1 col-md-10 col-lg-8
        blanc corps_page">
          <% if (nom_utilisateur !== 'session inexistante') { %>
          <input type="text" id="input_sujet" class="e"
          placeholder="Sujet de la discussion">
            <% include editeur %>
          <% } else { %>
            <script>
              window.location.assign('login');
            </script>
          <% } %>
        </div>
        <div class="col-lg-offset-2 col-md-offset-1"></div>
      </div>
    </div>
  </body>
</html>
./views/nouveau_compte.ejs
<!DOCTYPE html>
<html>
  <head>
    <% include balise_head %>
    <script src="static/js/login.js"></script>
    <script src="static/js/nouveau_compte.js"></script>
    <script src="static/js/verification_form.js"></script>
  </head>
  <body>
    <% include barre_navigation %>
    <div class="container">
      <div class="row">
        <div class="col-lg-offset-2 col-md-offset-1 col-md-10 col-lg-8 blanc">
        <center class="alert alert-danger" id="utilisateur_existant"
        style="display: none;">
          Ce nom d'utilisateur existe déjà. Veuillez en choisir un autre.
        </center>
        <form class="form-horizontal" style="padding-right:20px;">
          <div class="form-group">
            <h4 class="gros_titre">Créer un compte</h4>
          </div>
          <% include form_nouveau_compte %>
          <div class="form-group">
            <span class="pull-right btn btn-primary btn-nouveau_compte"
              onclick="nouveau_compte();">
                Créer un compte
            </span>
          </div>
        </form>
        <div class="col-lg-offset-2 col-md-offset-1"></div>
      </div>
    </div>
  </body>
</html>
./views/profil.ejs
<!DOCTYPE html>
<html lang="fr">
  <% if (!profil) { %>
    <script>
      window.location.assign('error');
    </script>
  <% } else { %>
    <head>
      <% include balise_head %>
      <script src="static/js/verification_form.js"></script>
      <script src="static/js/profil.js"></script>
      <script> // ces fonctions javascript ont besoin de variables EJS
        function inserer_form() {
          byId('nc_prenom').value = '<%= profil.prenom %>';
          byId('nc_nom').value = '<%= profil.nom %>';
          byId('nc_prenom_mobile').value = '<%= profil.prenom %>';
          byId('nc_nom_mobile').value = '<%= profil.nom %>';
          byId('nc_mail').value = '<%= profil.email %>';
        }
      </script>
    </head>

    <body>
      <% include barre_navigation %>
      <div class="container">
        <div class="row">
          <div class="col-lg-offset-2 col-md-offset-1 col-md-10 col-lg-8
          blanc corps_page">
            <div class="alert alert-success" style="text-align:center; display:none"
            id="coordonnees_success">
              Les coordonnées ont été modifiées avec succès
            </div>
            <div class="alert alert-success" style="text-align:center; display:none"
            id="password_success">
              Le mot de passe a été modifié avec succès
            </div>
            <h4 class="gros_titre">Profil</h4>
            <div class="avatar_profil_mobile">
              <img src="static/avatars_grands/<%= profil.avatar %>.jpg"
              class="avatar_profil">
            </div>
            <div class="row">
              <div class="col-xs-4 avatar_profil_desktop">
                <img src="static/avatars_grands/<%= profil.avatar %>.jpg"
                class="avatar_profil"><br>
              </div>
              <div class="col-xs-8" id="infos_profil">
                <p style="font-size:16px; margin-top:15px;">
                  <span id="info_nom_utilisateur">
                    Nom d'utilisateur : <%= profil.nom_utilisateur %>
                  </span><br>
                  <span id="info_prenom">
                    Prénom : <%= profil.prenom %>
                  </span><br>
                  <span id="info_nom">
                    Nom : <%= profil.nom %>
                  </span><br>
                  <span id="info_email">
                    E-mail : <%= profil.email %>
                  </span><br>
                </p>
                <% if (nom_utilisateur === profil.nom_utilisateur) { %>
                  <span class="btn btn-default" onclick="afficher_mod();">
                    <span class="glyphicon glyphicon-pencil"></span>
                    Coordonnées
                  </span>
                  <span class="btn btn-default" onclick="afficher_mod_password();">
                    <span class="glyphicon glyphicon-pencil"></span>
                    Mot de passe
                  </span>
                  <span class="btn btn-default" onclick="afficher_mod_avatar();">
                    <span class="glyphicon glyphicon-pencil"></span>
                    Avatar
                  </span>
                <% } else { %>
                  <a class="btn btn-primary" href="mailto:<%= profil.email %>">
                    <span class="glyphicon glyphicon-pencil"></span>
                    Envoyer un courriel
                  </a>
                <% } %>
              </div>
            </div>
            <% include form_profil %>
            <div id="mod_avatar" style="display:none;">
              <script> avatars_small('<%= nom_utilisateur %>'); </script>
            </div>
          </div>
          <div class="col-lg-offset-2 col-md-offset-1"></div>
        </div>
      </div>
      <script> /* ces fonctions doivent attendre que la page soit chargée
        pour être exécutées */
        responsive();
        inserer_form();
        verifier_form();
        afficher_mod();
      </script>
    </body>
  <% } %>
<html>