##### Code-source ##### .. captionup:: ./package.json .. code-block:: javascript { "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 ===== .. captionup:: ./app.js .. code-block:: javascript 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); .. captionup:: serveur/routes.js .. code-block:: javascript 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'); }); }; .. captionup:: ./serveur/session.js .. code-block:: javascript // 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); }); } .. captionup:: ./serveur/socket.js .. code-block:: javascript 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'); }); }); }); }); } .. captionup:: ./serveur/sql.js .. code-block:: javascript /* 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 ===== .. captionup:: ./static/js/conversion_dates.js .. code-block:: javascript 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; } } .. captionup:: ./static/js/discussion.js .. code-block:: javascript 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 = ''; zone_message('affichage', d).innerHTML = unechap(d.contenu); }); .. captionup:: ./static/js/general.js .. code-block:: javascript 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'; } } .. captionup:: ./static/js/index.js .. code-block:: javascript 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'; } } } .. captionup:: ./static/js/login.js .. code-block:: javascript // 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); }); .. captionup:: ./static/js/new.js .. code-block:: javascript // 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); }); .. captionup:: ./static/js/nouveau_compte.js .. code-block:: javascript // 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'; }); .. captionup:: ./static/js/profil.js .. code-block:: javascript /* change de classe bootstrap la balise
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 += ''; } } // 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 = ''; byClass('avatar_profil_mobile')[0].innerHTML = ''; 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'; } }); .. captionup:: ./static/js/verification_form.js .. code-block:: javascript 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; } .. captionup:: ./static/responsive.css .. code-block:: 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; } } .. captionup:: ./static/style.css .. code-block:: 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 ===== .. captionup:: ./views/balise_head.ejs .. code-block:: guess Forum tm 15-16 .. captionup:: ./views/barre_navigation.ejs .. code-block:: guess .. captionup:: ./views/chargement_discussions.ejs .. code-block:: guess <% convertir_date = function(date) { var id_temp = Math.random().toString(36).substring(7); %> <% } for (var i = 0; i < discussions.length; i++) { %>
<%= discussions[i][0].sujet %>
créé par <%= discussions[i][0].utilisateur_createur %> le <% convertir_date(discussions[i][0].date_ecriture); %> <%= discussions[i][0].nombre_messages %> <% if (discussions[i][0].nombre_messages === 1) { %> message <% } %> <% if (discussions[i][0].nombre_messages > 1) { %> messages <% } %>
<% } %> .. captionup:: ./views/chargement_messages.ejs .. code-block:: guess <% convertir_date = function(date) { var id_temp = Math.random().toString(36).substring(7); %> <% } for (var i = 0; i < m.length; i++) { %> <% m[i].contenu = m[i].contenu .replace(/#g_doubles/g, '"') .replace(/#g_simples/g, "'") .replace(/#backslash/g, "\\"); %>


<%= m[i].nom_utilisateur %> <% if (nom_utilisateur === m[i].nom_utilisateur) { %> modifier supprimer
<% } %>

<% if (nom_utilisateur === m[i].nom_utilisateur) { %>
<% } else { %>
<% } %>
écrit le <% convertir_date(m[i].date_ecriture); %>
<% if (m[i].date_modification !== '0000-00-00 00:00:00') { %> modifié le <% convertir_date(m[i].date_modification); %>
<% } %>
<%- m[i].contenu %>
<% if (nom_utilisateur === m[i].nom_utilisateur) { %>
modifier
supprimer
<% } %>
<% } %> .. captionup:: ./views/discussion.ejs .. code-block:: guess <% include balise_head %> <% include barre_navigation %>

<% if (nom_utilisateur === 'session inexistante') { %>

Vous n'êtes actuellement pas connecté. Par conséquent, vous ne pouvez pas aimer ou publier des messages. Connectez-vous pour participer à la discussion.
<% } %>

<%= sujet_discussion %>
<% include chargement_messages %>
<% if (nom_utilisateur !== 'session inexistante') { %> <% include editeur %> <% } %>
.. captionup:: ./views/editeur.ejs .. code-block:: guess

Vous pouvez agrandir la fenêtre Publier

.. captionup:: ./views/erreur_discussion.ejs .. code-block:: guess Cette discussion n'existe pas .. captionup:: ./views/error.ejs .. code-block:: guess Cette page n'existe pas .. captionup:: ./views/form_nouveau_compte.ejs .. code-block:: guess
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.
Ce prénom est invalide.
Ce nom est invalide.
Ce prénom est invalide.
Ce nom est invalide.
Cette adresse e-mail est invalide.
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.
.. captionup:: ./views/form_profil.ejs .. code-block:: guess

Modifier les coordonnées

Ce prénom est invalide.
Ce nom est invalide.
Ce prénom est invalide.
Ce nom est invalide.
Cette adresse e-mail est invalide.
Enregistrer

Modifier le mot de passe

Le mot de passe est 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.
Enregistrer
.. captionup:: ./views/index.ejs .. code-block:: guess <% include balise_head %> <% include barre_navigation %>

<% include chargement_discussions %>

.. captionup:: ./views/login.ejs .. code-block:: guess <% include balise_head %> <% include barre_navigation %>
Vous n'avez pas encore de compte ? Inscrivez-vous maintenant !

Se connecter

Le nom d'utilisateur ou le mot de passe est incorrect.
Login
.. captionup:: ./views/new.ejs .. code-block:: guess <% include balise_head %> <% include barre_navigation %>
<% if (nom_utilisateur !== 'session inexistante') { %> <% include editeur %> <% } else { %> <% } %>
.. captionup:: ./views/nouveau_compte.ejs .. code-block:: guess <% include balise_head %> <% include barre_navigation %>

Créer un compte

<% include form_nouveau_compte %>
Créer un compte
.. captionup:: ./views/profil.ejs .. code-block:: guess <% if (!profil) { %> <% } else { %> <% include balise_head %> <% include barre_navigation %>

Profil


Nom d'utilisateur : <%= profil.nom_utilisateur %>
Prénom : <%= profil.prenom %>
Nom : <%= profil.nom %>
E-mail : <%= profil.email %>

<% if (nom_utilisateur === profil.nom_utilisateur) { %> Coordonnées Mot de passe Avatar <% } else { %> Envoyer un courriel <% } %>
<% include form_profil %>
<% } %>