FIX problème de mise à jour du profile
[euphorik.git] / js / euphorik.js
1 // coding: utf-8
2 // Copyright 2008 Grégory Burri
3 //
4 // This file is part of Euphorik.
5 //
6 // Euphorik is free software: you can redistribute it and/or modify
7 // it under the terms of the GNU General Public License as published by
8 // the Free Software Foundation, either version 3 of the License, or
9 // (at your option) any later version.
10 //
11 // Euphorik is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 // GNU General Public License for more details.
15 //
16 // You should have received a copy of the GNU General Public License
17 // along with Euphorik. If not, see <http://www.gnu.org/licenses/>.
18
19 /**
20 * Contient la base javascript pour le site euphorik.ch.
21 * Chaque page possède son propre fichier js nommé "page<nom de la page>.js".
22 * Auteur : GBurri
23 * Date : 6.11.2007
24 */
25
26
27 /**
28 * La configuration.
29 * Normalement 'const' à la place de 'var' mais non supporté par IE7.
30 */
31 var conf = {
32 versionProtocole : 3, // version du protcole
33 nickDefaut : "<nick>",
34 nbMessageAffiche : 10, // (par page)
35 pseudoDefaut : "<nick>",
36 tempsAffichageMessageDialogue : 4000, // en ms
37 tempsKick : 15, // en minute
38 tempsBan : 60 * 24 * 3, // en minutes (3jours)
39 smiles : {
40 "smile" : [/:\)/g, /:-\)/g],
41 "bigsmile" : [/:D/g, /:-D/g],
42 "clin" : [/;\)/g, /;-\)/g],
43 "cool" : [/8\)/g, /8-\)/g],
44 "eheheh" : [/:P/g, /:-P/g],
45 "lol" : [/\[-lol\]/g],
46 "spliff" : [/\[-spliff\]/g],
47 "oh" : [/:o/g, /:O/g],
48 "heink" : [/\[-heink\]/g],
49 "hum" : [/\[-hum\]/g],
50 "boh" : [/\[-boh\]/g],
51 "sniff" : [/:\(/g, /:-\(/g],
52 "triste" : [/\[-triste\]/g],
53 "pascontent" : [/>\(/g, /&gt;\(/g],
54 "argn" : [/\[-argn\]/g],
55 "redface" : [/\[-redface\]/g],
56 "bunny" : [/\[-lapin\]/g],
57 "chat" : [/\[-chat\]/g],
58 "renne" : [/\[-renne\]/g],
59 "star" : [/\[-star\]/g],
60 "kirby" : [/\[-kirby\]/g],
61 "slurp" : [/\[-slurp\]/g],
62 "agreed" : [/\[-agreed\]/g],
63 "dodo" : [/\[-dodo\]/g],
64 "bn" : [/\[-bn\]/g]
65 }
66 }
67
68 ///////////////////////////////////////////////////////////////////////////////////////////////////
69
70 String.prototype.trim = function()
71 {
72 return jQuery.trim(this) // anciennement : this.replace(/^\s+|\s+$/g, "");
73 }
74
75 String.prototype.ltrim = function()
76 {
77 return this.replace(/^\s+/, "");
78 }
79
80 String.prototype.rtrim = function()
81 {
82 return this.replace(/\s+$/, "");
83 }
84
85 ///////////////////////////////////////////////////////////////////////////////////////////////////
86
87 /**
88 * Cette classe regroupe des fonctions utilitaires (helpers).
89 * @formateur est permet de formater les messages affichés à l'aide de messageDialogue (facultatif)
90 */
91 function Util(formateur)
92 {
93 $("#info .fermer").click(function(){
94 $("#info").slideUp(50)
95 })
96
97 $("body").append('<div id="flecheBulle"></div>').append('<div id="messageBulle"><p></p></div>')
98
99 this.formateur = formateur
100 this.bulleActive = true
101 }
102
103 var messageType = {informatif: 0, question: 1, erreur: 2}
104
105 /**
106 * Affiche une boite de dialogue avec un message à l'intérieur.
107 * @param message le message (string)
108 * @param type voir 'messageType'. par défaut messageType.informatif
109 * @param les boutons sous la forme d'un objet ou les clefs sont les labels des boutons
110 * et les valeurs les fonctions executées lorsqu'un bouton est activé.
111 * @param formate faut-il formaté le message ? true par défaut
112 */
113 Util.prototype.messageDialogue = function(message, type, boutons, formate)
114 {
115 var thisUtil = this
116
117 if (type == undefined)
118 type = messageType.informatif
119
120 if (formate == undefined)
121 formate = true
122
123 if (this.timeoutMessageDialogue != undefined)
124 clearTimeout(this.timeoutMessageDialogue)
125
126 var fermer = function(){$("#info").slideUp(100)}
127 fermer()
128
129 $("#info .message").html(thisUtil.formateur == undefined || !formate ? message : thisUtil.formateur.traitementComplet(message))
130 switch(type)
131 {
132 case messageType.informatif : $("#info #icone").attr("class", "information"); break
133 case messageType.question : $("#info #icone").attr("class", "interrogation"); break
134 case messageType.erreur : $("#info #icone").attr("class", "exclamation"); break
135 }
136 $("#info .boutons").html("")
137 for (var b in boutons)
138 $("#info .boutons").append("<div>" + b + "</div>").find("div:last").click(boutons[b]).click(fermer)
139
140 $("#info").slideDown(200)
141 this.timeoutMessageDialogue = setTimeout(fermer, conf.tempsAffichageMessageDialogue)
142 }
143
144 var positionTypeX = {gauche: 0, gaucheRecouvrement: 1, centre: 2, droiteRecouvrement: 3, droite: 4}
145 var positionTypeY = {haut: 0, hautRecouvrement: 1, centre: 2, basRecouvrement: 3, bas: 4}
146
147 /**
148 * Afficher une boite flottante (élément) par rapport à une cible.
149 * La boite est affichée de manière à ne pas dépasser de la fenêtre.
150 * @boite l'élément à afficher
151 * @cible l'élément sur lequel est affiché la boite
152 * @positionX de type positionTypeX
153 * @positionY de type positionTypeY
154 */
155 Util.prototype.afficherBoite = function(boite, cible, positionX, positionY)
156 {
157 var positionCible = cible.offset()
158 var positionBoite =
159 {
160 left : positionX == positionTypeX.gauche ? positionCible.left - boite.width() :
161 (positionX == positionTypeX.gaucheRecouvrement ? positionCible.left - boite.width() + cible.width() :
162 (positionX == positionTypeX.droitelsRecouvrement ? positionCible.left :
163 (positionX == positionTypeX.droite ? positionCible.left + cible.width() :
164 positionCible.left + cible.width() / 2 - boite.width() / 2 ))), // centre
165 top : positionY == positionTypeY.haut ? positionCible.top - boite.height() :
166 (positionY == positionTypeY.hautRecouvrement ? positionCible.top - boite.height() + cible.height() :
167 (positionY == positionTypeY.basRecouvrement ? positionCible.top :
168 (positionY == positionTypeY.bas ? positionCible.top + cible.height() :
169 positionCible.top + cible.height() / 2 - boite.height() / 2 ))) // centre
170 }
171
172 // calcul les décalages en x et en y pour éviter que la boite ne sorte de la fenêtre, tient compte de la position des barres de défilement
173 var marge = 10
174 positionBoite.left = positionBoite.left < marge + window.pageXOffset ? marge + window.pageXOffset :
175 (boite.width() - $(window).width() + (positionBoite.left - window.pageXOffset) + marge > 0 ? $(window).width() - boite.width() - marge + window.pageXOffset : positionBoite.left)
176 positionBoite.top = positionBoite.top < marge + window.pageYOffset ? marge + window.pageYOffset :
177 (boite.height() - $(window).height() + (positionBoite.top - window.pageYOffset) + marge > 0 ? $(window).height() - boite.height() - marge + window.pageYOffset : positionBoite.top)
178
179 boite.css("top", positionBoite.top).css("left", positionBoite.left).show()
180 }
181
182 /**
183 * Affiche un info bulle lorsque le curseur survole l'élément donné.
184 * FIXME : le width de element ne tient pas compte du padding !?
185 */
186 Util.prototype.infoBulle = function(message, element)
187 {
188 var thisUtil = this
189
190 var cacherBulle = function()
191 {
192 $("#flecheBulle").hide()
193 $("#messageBulle").hide()
194 }
195
196 element.hover(
197 function()
198 {
199 if (!thisUtil.bulleActive)
200 return
201
202 var m = $("#messageBulle")
203 var f = $("#flecheBulle")
204
205 // remplie le paragraphe de la bulle avec le message
206 $("p", m).html(message)
207
208 // réinitialise la position, évite le cas ou la boite est collé à droite et remplie avec un texte la faisant dépassé
209 // dans ce cas la hauteur n'est pas calculé correctement
210 m.css("top", 0).css("left", 0)
211
212 var positionFleche = {
213 left : element.offset().left + element.innerWidth() / 2 - f.width() / 2,
214 top : element.offset().top - f.height()
215 }
216 var positionMessage = {
217 left : element.offset().left + element.width() / 2 - m.width() / 2,
218 top : element.offset().top - f.height() - m.height()
219 }
220 var depassementDroit = (positionMessage.left + m.width()) - $("body").width()
221 if (depassementDroit > 0)
222 positionMessage.left -= depassementDroit
223 else
224 {
225 if (positionMessage.left < 0)
226 positionMessage.left = 0
227 }
228
229 m.css("top", positionMessage.top).css("left", positionMessage.left).show()
230 f.css("top", positionFleche.top).css("left", positionFleche.left).show()
231 },
232 cacherBulle
233 ).click(cacherBulle)
234 }
235
236 /**
237 * Utilisé pour l'envoie de donnée avec la méthode ajax de jQuery.
238 */
239 Util.prototype.jsonVersAction = function(json)
240 {
241 return {action : JSON.stringify(json) }
242 }
243
244 Util.prototype.md5 = function(chaine)
245 {
246 return hex_md5(chaine)
247 }
248
249 // pompé de http://www.faqts.com/knowledge_base/view.phtml/aid/13562/fid/130
250 Util.prototype.setSelectionRange = function(input, selectionStart, selectionEnd)
251 {
252 if (input.setSelectionRange)
253 {
254 input.focus()
255 input.setSelectionRange(selectionStart, selectionEnd)
256 }
257 else if (input.createTextRange)
258 {
259 var range = input.createTextRange()
260 range.collapse(true)
261 range.moveEnd('character', selectionEnd)
262 range.moveStart('character', selectionStart)
263 range.select()
264 }
265 }
266
267 Util.prototype.setCaretToEnd = function(input)
268 {
269 this.setSelectionRange(input, input.value.length, input.value.length)
270 }
271 Util.prototype.setCaretToBegin = function(input)
272 {
273 this.setSelectionRange(input, 0, 0)
274 }
275 Util.prototype.setCaretToPos = function(input, pos)
276 {
277 this.setSelectionRange(input, pos, pos)
278 }
279 Util.prototype.selectString = function(input, string)
280 {
281 var match = new RegExp(string, "i").exec(input.value)
282 if (match)
283 {
284 this.setSelectionRange (input, match.index, match.index + match[0].length)
285 }
286 }
287 Util.prototype.replaceSelection = function(input, replaceString) {
288 if (input.setSelectionRange)
289 {
290 var selectionStart = input.selectionStart
291 var selectionEnd = input.selectionEnd
292 input.value = input.value.substring(0, selectionStart) + replaceString + input.value.substring(selectionEnd)
293
294 if (selectionStart != selectionEnd) // has there been a selection
295 this.setSelectionRange(input, selectionStart, selectionStart + replaceString.length)
296 else // set caret
297 this.setCaretToPos(input, selectionStart + replaceString.length)
298 }
299 else if (document.selection)
300 {
301 input.focus()
302 var range = document.selection.createRange()
303 if (range.parentElement() == input)
304 {
305 var isCollapsed = range.text == ''
306 range.text = replaceString
307 if (!isCollapsed)
308 {
309 range.moveStart('character', -replaceString.length);
310 }
311 }
312 }
313 }
314
315 Util.prototype.rot13 = function(chaine)
316 {
317 var ACode = 'A'.charCodeAt(0)
318 var aCode = 'a'.charCodeAt(0)
319 var MCode = 'M'.charCodeAt(0)
320 var mCode = 'm'.charCodeAt(0)
321 var ZCode = 'Z'.charCodeAt(0)
322 var zCode = 'z'.charCodeAt(0)
323
324 var f = function(ch, pos) {
325 if (pos == ch.length)
326 return ""
327
328 var c = ch.charCodeAt(pos);
329 return String.fromCharCode(
330 c +
331 (c >= ACode && c <= MCode || c >= aCode && c <= mCode ? 13 :
332 (c > MCode && c <= ZCode || c > mCode && c <= zCode ? -13 : 0))
333 ) + f(ch, pos + 1)
334 }
335 return f(chaine, 0)
336 }
337
338 ///////////////////////////////////////////////////////////////////////////////////////////////////
339
340 function Pages()
341 {
342 this.pageCourante = null
343 this.pages = {}
344 }
345
346 /**
347 * Accepte soit un objet soit un string.
348 * un string correspond au nom de la page, par exemple : "page" -> "page.html"
349 */
350 Pages.prototype.ajouterPage = function(page)
351 {
352 if (typeof page == "string")
353 {
354 this.pages[page] = page
355 }
356 else
357 {
358 page.pages = this // la magie des langages dynamiques : le foutoire
359 this.pages[page.nom] = page
360 }
361 }
362
363 Pages.prototype.afficherPage = function(nomPage, forcerChargement)
364 {
365 if (forcerChargement == undefined) forcerChargement = false
366
367 var page = this.pages[nomPage]
368 if (page == undefined || (!forcerChargement && page == this.pageCourante)) return
369
370 if (this.pageCourante != null && this.pageCourante.decharger)
371 this.pageCourante.decharger()
372
373 $("#menu li").removeClass("courante")
374 $("#menu li." + nomPage).addClass("courante")
375
376 this.pageCourante = page
377 var contenu = ""
378 if (typeof page == "string")
379 $.ajax({async: false, url: "pages/" + page + ".html", success : function(page) { contenu += page }})
380 else
381 contenu += this.pageCourante.contenu()
382 $("#page").html(contenu).removeClass().addClass(this.pageCourante.nom)
383
384 if (this.pageCourante.charger)
385 this.pageCourante.charger()
386 }
387
388 ///////////////////////////////////////////////////////////////////////////////////////////////////
389
390 /**
391 * Classe permettant de formater du texte par exemple pour la substitution des liens dans les
392 * message par "[url]".
393 * TODO : améliorer l'efficacité des méthods notamment lié au smiles.
394 */
395 function Formateur()
396 {
397 this.smiles = conf.smiles
398 this.protocoles = "http|https|ed2k"
399
400 this.regexUrl = new RegExp("(?:(?:" + this.protocoles + ")://|www\\.)[^ ]*", "gi")
401 this.regexImg = new RegExp("^.*?\\.(gif|jpg|png|jpeg|bmp|tiff)$", "i")
402 this.regexDomaine = new RegExp("^(?:(?:" + this.protocoles + ")://|www\\.).*?([^/.]+\\.[^/.]+)(?:$|/).*$", "i")
403 this.regexTestProtocoleExiste = new RegExp("^(?:" + this.protocoles + ")://.*$", "i")
404 this.regexNomProtocole = new RegExp("^(.*?)://")
405 }
406
407 /**
408 * Formate un pseudo saise par l'utilisateur.
409 * @param pseudo le pseudo brut
410 * @return le pseudo filtré
411 */
412 Formateur.prototype.filtrerInputPseudo = function(pseudo)
413 {
414 return pseudo.replace(/{|}/g, "").trim()
415 }
416
417 Formateur.prototype.getSmilesHTML = function()
418 {
419 var XHTML = ""
420 for (var sNom in this.smiles)
421 {
422 XHTML += "<img class=\"" + sNom + "\" src=\"img/smileys/" + sNom + ".gif\" alt =\"" + sNom + "\" />"
423 }
424 return XHTML
425 }
426
427 /**
428 * Formatage complet d'un texte.
429 * @M le message
430 * @pseudo facultatif, permet de contruire le label des images sous la forme : "<Pseudo> : <Message>"
431 */
432 Formateur.prototype.traitementComplet = function(M, pseudo)
433 {
434 return this.traiterLiensConv(this.traiterSmiles(this.traiterURL(this.traiterWikiSyntaxe(this.remplacerBalisesHTML(M)), pseudo)))
435 }
436
437 /**
438 * Transforme les liens en entités clickables.
439 * Un lien vers une conversation permet d'ouvrire celle ci, elle se marque comme ceci dans un message :
440 * "{5F}" ou 5F est la racine de la conversation.
441 * Ce lien sera transformer en <span class="lienConv">{5F}</span> pouvant être clické pour créer la conv 5F.
442 */
443 Formateur.prototype.traiterLiensConv = function(M)
444 {
445 return M.replace(
446 /\{\w+\}/g,
447 function(lien)
448 {
449 return "<span class=\"lienConv\">" + lien + "</span>"
450 }
451 )
452 }
453
454 /**
455 * FIXME : Cette méthode est attrocement lourde ! A optimiser.
456 * moyenne sur échantillon : 234ms
457 */
458 Formateur.prototype.traiterSmiles = function(M)
459 {
460 for (var sNom in this.smiles)
461 {
462 ss = this.smiles[sNom]
463 for (var i = 0; i < ss.length; i++)
464 M = M.replace(ss[i], "<img src=\"img/smileys/" + sNom + ".gif\" alt =\"" + sNom + "\" />")
465 }
466 return M
467 }
468
469 Formateur.prototype.remplacerBalisesHTML = function(M)
470 {
471 return M.replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;")
472 }
473
474 Formateur.prototype.traiterURL = function(M, pseudo)
475 {
476 thisFormateur = this
477
478 var traitementUrl = function(url)
479 {
480 // si ya pas de protocole on rajoute "http://"
481 if (!thisFormateur.regexTestProtocoleExiste.test(url))
482 url = "http://" + url
483 var extension = thisFormateur.getShort(url)
484 return "<a " + (extension[1] ? "title=\"" + (pseudo == undefined ? "" : thisFormateur.traiterPourFenetreLightBox(pseudo, url) + ": ") + thisFormateur.traiterPourFenetreLightBox(M, url) + "\"" + " rel=\"lightbox\"" : "") + " href=\"" + url + "\" >[" + extension[0] + "]</a>"
485 }
486 return M.replace(this.regexUrl, traitementUrl)
487 }
488
489 /**
490 * Formatage en utilisant un sous-ensemble des règles de mediwiki.
491 * par exemple ''italic'' devient <i>italic</i>
492 */
493 Formateur.prototype.traiterWikiSyntaxe = function(M)
494 {
495 return M.replace(
496 /'''(.*?)'''/g,
497 function(texte, capture)
498 {
499 return "<b>" + capture + "</b>"
500 }
501 ).replace(
502 /''(.*?)''/g,
503 function(texte, capture)
504 {
505 return "<i>" + capture + "</i>"
506 }
507 )
508 }
509
510 /**
511 * Renvoie une version courte de l'url.
512 * par exemple : http://en.wikipedia.org/wiki/Yakov_Smirnoff devient wikipedia.org
513 */
514 Formateur.prototype.getShort = function(url)
515 {
516 var estUneImage = false
517 var versionShort = null
518 var rechercheImg = this.regexImg.exec(url)
519
520 if (rechercheImg != null)
521 {
522 versionShort = rechercheImg[1].toLowerCase()
523 if (versionShort == "jpeg") versionShort = "jpg" // jpeg -> jpg
524 estUneImage = true
525 }
526 else
527 {
528 var rechercheDomaine = this.regexDomaine.exec(url)
529 if (rechercheDomaine != null && rechercheDomaine.length >= 2)
530 versionShort = rechercheDomaine[1]
531 else
532 {
533 var nomProtocole = this.regexNomProtocole.exec(url)
534 if (nomProtocole != null && nomProtocole.length >= 2)
535 versionShort = nomProtocole[1]
536 }
537 }
538
539 return [versionShort == null ? "url" : versionShort, estUneImage]
540 }
541
542 /**
543 * Traite les pseudo et messages à être affiché dans le titre d'une image visualisé avec lightbox.
544 */
545 Formateur.prototype.traiterPourFenetreLightBox = function(M, urlCourante)
546 {
547 thisFormateur = this
548 var traitementUrl = function(url)
549 {
550 return "[" + thisFormateur.getShort(url)[0] + (urlCourante == url ? "*" : "") + "]"
551 }
552
553 return this.remplacerBalisesHTML(M).replace(this.regexUrl, traitementUrl)
554 }
555
556
557 ///////////////////////////////////////////////////////////////////////////////////////////////////
558
559 // les statuts possibes du client
560 var statutType = {
561 // mode enregistré, peut poster des messages et modifier son profile
562 auth_registered : 0,
563 // mode identifié, peut poster des messages mais n'a pas accès au profile
564 auth_not_registered : 1,
565 // mode déconnecté, ne peut pas poster de message
566 deconnected : 2
567 }
568
569 function Client(util)
570 {
571 this.util = util
572
573 this.cookie = null
574 this.regexCookie = new RegExp("^cookie=([^;]*)")
575
576 // données personnels
577 this.resetDonneesPersonnelles()
578
579 this.setStatut(statutType.deconnected)
580
581 // si true alors chaque modification du client est mémorisé sur le serveur
582 this.autoflush = $.browser["opera"]
583 }
584
585 Client.prototype.resetDonneesPersonnelles = function()
586 {
587 this.id = 0
588 this.pseudo = conf.pseudoDefaut
589 this.login = ""
590 this.password = ""
591 this.email = ""
592 this.css = $("link#cssPrincipale").attr("href")
593 this.nickFormat = "nick"
594 this.viewTimes = true
595 this.viewTooltips = true
596 this.cookie = undefined
597
598 this.pagePrincipale = 1
599 this.ekMaster = false
600 this.ostentatiousMaster = "light"
601
602 // les conversations, une conversation est un objet possédant les attributs suivants :
603 // - root (entier)
604 // - page (entier)
605 // - reduit (bool)
606 this.conversations = []
607 }
608
609 Client.prototype.setCss = function(css)
610 {
611 if (this.css == css || css == "")
612 return
613
614 this.css = css
615 $("link#cssPrincipale").attr("href", this.css)
616 if (this.autoflush) this.flush(true)
617 }
618
619 Client.prototype.pageSuivante = function(numConv)
620 {
621 if (numConv < 0 && this.pagePrincipale > 1)
622 this.pagePrincipale -= 1
623 else if (this.conversations[numConv].page > 1)
624 this.conversations[numConv].page -= 1
625 }
626
627 Client.prototype.pagePrecedente = function(numConv)
628 {
629 if (numConv < 0)
630 this.pagePrincipale += 1
631 else
632 this.conversations[numConv].page += 1
633 }
634
635 /**
636 * Définit la première page pour la conversation donnée.
637 * @return true si la page a changé sinon false
638 */
639 Client.prototype.goPremierePage = function(numConv)
640 {
641 if (numConv < 0)
642 {
643 if (this.pagePrincipale == 1)
644 return false
645 this.pagePrincipale = 1
646 }
647 else
648 {
649 if (this.conversations[numConv].page == 1)
650 return false
651 this.conversations[numConv].page = 1
652 }
653 return true
654 }
655
656 /**
657 * Ajoute une conversation à la vue de l'utilisateur.
658 * Le profile de l'utilisateur est directement sauvegardé sur le serveur.
659 * @param racines la racine de la conversation (integer)
660 * @return true si la conversation a été créée sinon false (par exemple si la conv existe déjà)
661 */
662 Client.prototype.ajouterConversation = function(racine)
663 {
664 // vérification s'il elle n'existe pas déjà
665 for (var i = 0; i < this.conversations.length; i++)
666 if (this.conversations[i].root == racine)
667 return false
668
669 this.conversations.push({root : racine, page : 1, reduit : false})
670 if (this.autoflush) this.flush(true)
671
672 return true
673 }
674
675 Client.prototype.supprimerConversation = function(num)
676 {
677 if (num < 0 || num >= this.conversations.length) return
678
679 // décalage TODO : supprimer le dernier élément
680 for (var i = num; i < this.conversations.length - 1; i++)
681 this.conversations[i] = this.conversations[i+1]
682 this.conversations.pop()
683
684 if (this.autoflush) this.flush(true)
685 }
686
687 Client.prototype.getJSONLogin = function(login, password)
688 {
689 return {
690 "header" : { "action" : "authentification", "version" : conf.versionProtocole },
691 "login" : login,
692 "password" : password
693 }
694 }
695
696 Client.prototype.getJSONLoginCookie = function()
697 {
698 return {
699 "header" : { "action" : "authentification", "version" : conf.versionProtocole },
700 "cookie" : this.cookie
701 }
702 }
703
704 /**
705 * le couple (login, password) est facultatif. S'il n'est pas fournit alors il ne sera pas possible
706 * de s'autentifier avec (login, password).
707 */
708 Client.prototype.getJSONEnregistrement = function(login, password)
709 {
710 var mess = { "header" : { "action" : "register", "version" : conf.versionProtocole }}
711
712 if (login != undefined && password != undefined)
713 {
714 mess["login"] = login
715 mess["password"] = password
716 }
717
718 return mess;
719 }
720
721 Client.prototype.getJSONConversations = function()
722 {
723 var conversations = new Array()
724 for (var i = 0; i < this.conversations.length; i++)
725 conversations.push({root : this.conversations[i].root, minimized : this.conversations[i].reduit})
726 return conversations
727 }
728
729 Client.prototype.getJSONProfile = function()
730 {
731 return {
732 "header" : { "action" : "set_profile", "version" : conf.versionProtocole },
733 "cookie" : this.cookie,
734 "login" : this.login,
735 "password" : this.password,
736 "nick" : this.pseudo,
737 "email" : this.email,
738 "css" : this.css,
739 "nick_format" : this.nickFormat,
740 "view_times" : this.viewTimes,
741 "view_tooltips" : this.viewTooltips,
742 "conversations" : this.getJSONConversations(),
743 "ostentatious_master" : this.ostentatiousMaster
744 }
745 }
746
747 /**
748 * Renvoie null si pas définit.
749 */
750 Client.prototype.getCookie = function()
751 {
752 var cookie = this.regexCookie.exec(document.cookie)
753 if (cookie == null) this.cookie = null
754 else this.cookie = cookie[1]
755 }
756
757 Client.prototype.delCookie = function()
758 {
759 document.cookie = "cookie=; max-age=0"
760 }
761
762 Client.prototype.setCookie = function()
763 {
764 if (this.cookie == null || this.cookie == undefined)
765 return
766
767 // ne fonctionne pas sous IE....
768 /*document.cookie = "cookie=" + this.cookie + "; max-age=" + (60 * 60 * 24 * 365) */
769
770 document.cookie =
771 "cookie="+this.cookie+"; expires=" + new Date(new Date().getTime() + 1000 * 60 * 60 * 24 * 365).toUTCString()
772 }
773
774 Client.prototype.authentifie = function()
775 {
776 return this.statut == statutType.auth_registered || this.statut == statutType.auth_not_registered
777 }
778
779 Client.prototype.setStatut = function(statut)
780 {
781 // conversation en "enum" si en "string"
782 if (typeof(statut) == "string")
783 {
784 statut =
785 statut == "auth_registered" ?
786 statutType.auth_registered :
787 (statut == "auth_not_registered" ? statutType.auth_not_registered : statutType.deconnected)
788 }
789
790 if (statut == this.statut) return
791
792 this.statut = statut
793 this.majMenu()
794 this.majLogo()
795 }
796
797 /**
798 * Effectue la connexion vers le serveur.
799 * Cette fonction est bloquante tant que la connexion n'a pas été établie.
800 * S'il existe un cookie en local on s'authentifie directement avec lui.
801 * Si il n'est pas possible de s'authentifier alors on affiche un captcha anti-bot.
802 */
803 Client.prototype.connexionCookie = function()
804 {
805 this.getCookie()
806 if (this.cookie == null) return false;
807 return this.connexion(this.getJSONLoginCookie())
808 }
809
810 Client.prototype.connexionLogin = function(login, password)
811 {
812 return this.connexion(this.getJSONLogin(login, password))
813 }
814
815 Client.prototype.enregistrement = function(login, password)
816 {
817 if (this.authentifie())
818 {
819 this.login = login
820 this.password = password
821 if(this.flush())
822 {
823 this.setStatut(statutType.auth_registered)
824 return true
825 }
826 return false
827 }
828 else
829 {
830 return this.connexion(this.getJSONEnregistrement(login, password))
831 }
832 }
833
834 Client.prototype.connexion = function(messageJson)
835 {
836 thisClient = this
837 jQuery.ajax(
838 {
839 async: false,
840 type: "POST",
841 url: "request",
842 dataType: "json",
843 data: this.util.jsonVersAction(messageJson),
844 success:
845 function(data)
846 {
847 if (data["reply"] == "error")
848 thisClient.util.messageDialogue(data["error_message"])
849 else
850 thisClient.chargerDonnees(data)
851 }
852 }
853 )
854 return this.authentifie()
855 }
856
857 Client.prototype.deconnexion = function()
858 {
859 this.flush(true)
860 this.delCookie()
861 this.resetDonneesPersonnelles()
862 this.setStatut(statutType.deconnected) // deconnexion
863 }
864
865 Client.prototype.chargerDonnees = function(data)
866 {
867 // la modification du statut qui suit met à jour le menu, le menu dépend (page admin)
868 // de l'état ekMaster
869 this.ekMaster = data["ek_master"] != undefined ? data["ek_master"] : false
870
871 this.setStatut(data["status"])
872
873 if (this.authentifie())
874 {
875 this.cookie = data["cookie"]
876 this.setCookie()
877
878 this.id = data["id"]
879 this.login = data["login"]
880 this.pseudo = data["nick"]
881 this.email = data["email"]
882 this.setCss(data["css"])
883 this.nickFormat = data["nick_format"]
884 this.viewTimes = data["view_times"]
885 this.viewTooltips = data["view_tooltips"]
886 this.ostentatiousMaster = data["ostentatious_master"]
887
888 // la page de la conversation principale
889 this.pagePrincipale = 1
890
891 // les conversations
892 this.conversations = data["conversations"]
893 for (var i = 0; i < this.conversations.length; i++)
894 this.conversations[i] = {root : this.conversations[i].root, page : 1, reduit : this.conversations[i].minimized}
895
896 this.majBulle()
897 this.majCssSelectionee()
898 //this.majLogo()
899 }
900 }
901
902 /**
903 * Met à jour les données personne sur serveur.
904 * @param async de manière asynchrone ? défaut = true
905 * @return false si le flush n'a pas pû se faire sinon true
906 */
907 Client.prototype.flush = function(async)
908 {
909 if (async == undefined)
910 async = false
911
912 if (!this.authentifie())
913 return false
914
915 var thisClient = this
916 var ok = true
917
918 jQuery.ajax(
919 {
920 async: async,
921 type: "POST",
922 url: "request",
923 dataType: "json",
924 data: this.util.jsonVersAction(this.getJSONProfile()),
925 success:
926 function(data)
927 {
928 if (data["reply"] == "error")
929 {
930 thisClient.util.messageDialogue(data["error_message"])
931 ok = false
932 }
933 else
934 {
935 thisClient.majBulle()
936 }
937 }
938 }
939 )
940
941 return ok
942 }
943
944 Client.prototype.majMenu = function()
945 {
946 displayType = "block"
947
948 $("#menu .admin").css("display", this.ekMaster ? displayType : "none")
949
950 // met à jour le menu
951 if (this.statut == statutType.auth_registered)
952 {
953 $("#menu .profile").css("display", displayType).text("profile")
954 $("#menu .logout").css("display", displayType)
955 $("#menu .register").css("display", "none")
956 }
957 else if (this.statut == statutType.auth_not_registered)
958 {
959 $("#menu .profile").css("display", "none")
960 $("#menu .logout").css("display", displayType)
961 $("#menu .register").css("display", displayType)
962 }
963 else
964 {
965 $("#menu .profile").css("display", displayType).text("login")
966 $("#menu .logout").css("display", "none")
967 $("#menu .register").css("display", displayType)
968 }
969 }
970
971 /**
972 * Met à jour l'affichage des infos bulles en fonction du profile.
973 */
974 Client.prototype.majBulle = function()
975 {
976 this.util.bulleActive = this.viewTooltips
977 }
978
979 /**
980 * Met à jour la css sélectionnée, lors du chargement des données.
981 */
982 Client.prototype.majCssSelectionee = function()
983 {
984 // extraction du numéro de la css courante
985 var numCssCourante = this.css.match(/^.*?\/(\d)\/.*$/)
986 if (numCssCourante != null && numCssCourante[1] != undefined)
987 {
988 $("#menuCss option").removeAttr("selected")
989 $("#menuCss option[value=" + numCssCourante[1]+ "]").attr("selected", "selected")
990 }
991 }
992
993 /**
994 * Change la "class" du logo en fonction du statut de ekMaster.
995 */
996 Client.prototype.majLogo = function()
997 {
998 if (this.ekMaster)
999 $("#logo").addClass("ekMaster")
1000 else
1001 $("#logo").removeClass("ekMaster")
1002 }
1003
1004
1005 Client.prototype.slap = function(userId, raison)
1006 {
1007 var thisClient = this
1008
1009 jQuery.ajax({
1010 type: "POST",
1011 url: "request",
1012 dataType: "json",
1013 data: this.util.jsonVersAction(
1014 {
1015 "header" : { "action" : "slap", "version" : conf.versionProtocole },
1016 "cookie" : thisClient.cookie,
1017 "user_id" : userId,
1018 "reason" : raison
1019 }),
1020 success:
1021 function(data)
1022 {
1023 if (data["reply"] == "error")
1024 thisClient.util.messageDialogue(data["error_message"])
1025 }
1026 })
1027 }
1028
1029 Client.prototype.ban = function(userId, raison, minutes)
1030 {
1031 var thisClient = this
1032
1033 // par défaut un ban correspond à 3 jours
1034 if (typeof(minutes) == "undefined")
1035 minutes = conf.tempsBan;
1036
1037 jQuery.ajax({
1038 type: "POST",
1039 url: "request",
1040 dataType: "json",
1041 data: this.util.jsonVersAction(
1042 {
1043 "header" : { "action" : "ban", "version" : conf.versionProtocole },
1044 "cookie" : thisClient.cookie,
1045 "duration" : minutes,
1046 "user_id" : userId,
1047 "reason" : raison
1048 }),
1049 success:
1050 function(data)
1051 {
1052 if (data["reply"] == "error")
1053 thisClient.util.messageDialogue(data["error_message"])
1054 }
1055 })
1056 }
1057
1058 Client.prototype.kick = function(userId, raison)
1059 {
1060 this.ban(userId, raison, conf.tempsKick)
1061 }
1062
1063 ///////////////////////////////////////////////////////////////////////////////////////////////////
1064
1065 /**
1066 * classe permettant de gérer les événements (push serveur).
1067 * l'information envoyé est sous la forme :
1068 * {
1069 * "header" : {"action" : "wait_event", "version" : <v> },
1070 * "page" : <page>
1071 * [..]
1072 * }
1073 * l'information reçu est sous la forme :
1074 * {
1075 * "reply" : <reply>
1076 * }
1077 * @page la page
1078 */
1079 function PageEvent(page, util)
1080 {
1081 this.page = page
1082 this.util = util
1083
1084 // l'objet JSONHttpRequest représentant la connexion d'attente
1085 this.attenteCourante = null
1086
1087 // le multhreading du pauvre, merci javascript de m'offrire autant de primitives pour la gestion de la concurrence...
1088 this.stop = false
1089 }
1090
1091 /**
1092 * Arrête l'attente courante s'il y en a une.
1093 */
1094 PageEvent.prototype.stopAttenteCourante = function()
1095 {
1096 this.stop = true
1097
1098 if (this.attenteCourante != null)
1099 {
1100 this.attenteCourante.abort()
1101 }
1102 }
1103
1104 /**
1105 * Attend un événement lié à la page.
1106 * @funSend une fonction renvoyant les données json à envoyer
1107 * @funsReceive est un objet comprenant les fonctions à appeler en fonction du "reply"
1108 * les fonctions acceptent un paramètre correspondant au données reçues.
1109 * exemple : {"new_message" : function(data){ ... }}
1110 */
1111 PageEvent.prototype.waitEvent = function(funSend, funsReceive)
1112 {
1113 this.stopAttenteCourante()
1114
1115 this.stop = false
1116
1117 var thisPageEvent = this
1118
1119 // on doit conserver l'ordre des valeurs de l'objet JSON (le serveur les veut dans l'ordre définit dans le protocole)
1120 // TODO : ya pas mieux ?
1121 var dataToSend =
1122 {
1123 "header" : { "action" : "wait_event", "version" : conf.versionProtocole },
1124 "page" : this.page
1125 }
1126 var poulpe = funSend()
1127 for (v in poulpe)
1128 dataToSend[v] = poulpe[v]
1129
1130 this.attenteCourante = jQuery.ajax({
1131 type: "POST",
1132 url: "request",
1133 dataType: "json",
1134 // TODO : doit disparaitre
1135 timeout: 180000, // timeout de 3min. Gros HACK pas beau. FIXME problème décrit ici : http://groups.google.com/group/jquery-en/browse_thread/thread/8724e64af3333a76
1136 data: this.util.jsonVersAction(dataToSend),
1137 success:
1138 function(data)
1139 {
1140 funsReceive[data["reply"]](data)
1141
1142 // rappel de la fonction dans 100 ms
1143 setTimeout(function(){ thisPageEvent.waitEvent2(funSend, funsReceive) }, 100)
1144 },
1145 error:
1146 function(XMLHttpRequest, textStatus, errorThrown)
1147 {
1148 ;; console.log("Connexion perdue dans waitEvent")
1149 setTimeout(function(){ thisPageEvent.waitEvent2(funSend, funsReceive) }, 1000)
1150 }
1151 })
1152 }
1153
1154 /**
1155 * Si un stopAttenteCourante survient un peu n'importe quand il faut imédiatement arreter de boucler.
1156 */
1157 PageEvent.prototype.waitEvent2 = function(funSend, funsReceive)
1158 {
1159 if (this.stop)
1160 return
1161 this.waitEvent(funSend, funsReceive)
1162 }
1163
1164 ///////////////////////////////////////////////////////////////////////////////////////////////////
1165
1166 function initialiserListeStyles(client)
1167 {
1168 $("#menuCss").change(
1169 function()
1170 {
1171 client.setCss("css/" + $("option:selected", this).attr("value") + "/euphorik.css")
1172 }
1173 )
1174 }
1175
1176 // charge dynamiquement le script de debug
1177 ;; jQuery.ajax({async : false, url : "js/debug.js", dataType : "script"})
1178
1179 // le main
1180 $(document).ready(
1181 function()
1182 {
1183 var formateur = new Formateur()
1184 var util = new Util(formateur)
1185 var client = new Client(util)
1186 var pages = new Pages()
1187
1188 // connexion vers le serveur (utilise un cookie qui traine)
1189 client.connexionCookie()
1190
1191 initialiserListeStyles(client)
1192
1193 // FIXME : ne fonctionne pas sous opera
1194 // voir : http://dev.jquery.com/ticket/2892#preview
1195 $(window).unload(function(){client.flush()})
1196
1197 $("#menu .minichat").click(function(){ pages.afficherPage("minichat") })
1198 $("#menu .admin").click(function(){ pages.afficherPage("admin") })
1199 $("#menu .profile").click(function(){ pages.afficherPage("profile") })
1200 $("#menu .logout").click(function(){
1201 util.messageDialogue("Êtes-vous sur de vouloir vous délogger ?", messageType.question,
1202 {"Oui" : function()
1203 {
1204 client.deconnexion();
1205 pages.afficherPage("minichat", true)
1206 },
1207 "Non" : function(){}
1208 }
1209 )
1210 })
1211 $("#menu .register").click(function(){ pages.afficherPage("register") })
1212 $("#menu .about").click(function(){ pages.afficherPage("about") })
1213
1214 // TODO : simplifier et pouvoir créer des liens par exemple : <span class="lien" href="conditions">Conditions d'utilisation</span>
1215 $("#footer .conditions").click(function(){ pages.afficherPage("conditions_utilisation") })
1216
1217 pages.ajouterPage(new PageMinichat(client, formateur, util))
1218 pages.ajouterPage(new PageAdmin(client, formateur, util))
1219 pages.ajouterPage(new PageProfile(client, formateur, util))
1220 pages.ajouterPage(new PageRegister(client, formateur, util))
1221 pages.ajouterPage(new PageAbout(client, formateur, util))
1222 pages.ajouterPage("conditions_utilisation")
1223
1224 pages.afficherPage("minichat")
1225 }
1226 )