MOD minimodif
[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 // tout euphorik est contenu dans cet objet
27 var euphorik = {}
28 // ;; euphorik.include =
29 // ;; euphorik.include = function(f) { var s = document.createElement('script'); s.type = 'text/javascript'; s.src = "js/" + f + ".js"; document.getElementsByTagName('head')[0].appendChild(s); }
30
31
32 // version jQuery : function(f) { jQuery.ajax({async : false, url : "js/" + f + ".js", dataType : "script"}) }
33 // mais comme il n'est pas encore chargé...
34 ;; euphorik.include = function(f) {
35 ;; var req, url = 'js/' + f + '.js'
36 ;; if (window.XMLHttpRequest) {
37 ;; req = new XMLHttpRequest(); req.open("GET", url, false /* async */); req.send(null);
38 ;; } else if (window.ActiveXObject) {
39 ;; req = new ActiveXObject((navigator.userAgent.toLowerCase().indexOf('msie 5') != -1) ? "Microsoft.XMLHTTP" : "Msxml2.XMLHTTP");
40 ;; if (req) { req.open("GET", url, false); req.send(); }
41 ;; }
42 ;; if (req!==false) { if (req.status==200) { window.eval(req.responseText); } else if (req.status==404) { alert("erreur de chargement (404) de : " + url) } }
43 ;; }
44
45 ;; euphorik.include("jquery")
46 ;; euphorik.include("jquery.lightbox")
47 ;; euphorik.include("md5")
48 ;; euphorik.include("json2")
49
50 ;; euphorik.include("conf")
51 ;; euphorik.include("util")
52 ;; euphorik.include("formateur")
53 ;; euphorik.include("pages")
54
55 ;; euphorik.include("pageMinichat")
56 ;; euphorik.include("pageAdmin")
57 ;; euphorik.include("pageProfile")
58 ;; euphorik.include("pageRegister")
59 ;; euphorik.include("pageAbout")
60
61
62 // tout un tas d'améliorations des objets javascript ;)
63 /**
64 * Pour chaque propriété de l'objet execute f(p, v) ou p est le nom de la propriété et v sa valeur.
65 * Ne parcours pas les propriétés des prototypes.
66 */
67 object.prototype.each = function(f) {
68 for (var b in boutons) {
69 if (boutons.hasOwnProperty(b)) {
70 f(b, boutons[b])
71 }
72 }
73 }
74
75 String.prototype.trim = function() {
76 return jQuery.trim(this) // anciennement : this.replace(/^\s+|\s+$/g, "");
77 }
78
79 String.prototype.ltrim = function() {
80 return this.replace(/^\s+/, "");
81 }
82
83 String.prototype.rtrim = function() {
84 return this.replace(/\s+$/, "");
85 }
86
87
88 ///////////////////////////////////////////////////////////////////////////////////////////////////
89
90 // les statuts possibes du client
91 var statutType = {
92 // mode enregistré, peut poster des messages et modifier son profile
93 auth_registered : 0,
94 // mode identifié, peut poster des messages mais n'a pas accès au profile
95 auth_not_registered : 1,
96 // mode déconnecté, ne peut pas poster de message
97 deconnected : 2
98 }
99
100 function Client(util)
101 {
102 this.util = util
103
104 this.cookie = null
105 this.regexCookie = new RegExp("^cookie=([^;]*)")
106
107 // données personnels
108 this.resetDonneesPersonnelles()
109
110 this.setStatut(statutType.deconnected)
111
112 // si true alors chaque modification du client est mémorisé sur le serveur
113 this.autoflush = $.browser["opera"]
114 }
115
116 Client.prototype.resetDonneesPersonnelles = function()
117 {
118 this.id = 0
119 this.pseudo = euphorik.conf.pseudoDefaut
120 this.login = ""
121 this.password = ""
122 this.email = ""
123 this.css = $("link#cssPrincipale").attr("href")
124 this.chatOrder = "reverse"
125 this.nickFormat = "nick"
126 this.viewTimes = true
127 this.viewTooltips = true
128 this.cookie = undefined
129
130 this.pagePrincipale = 1
131 this.ekMaster = false
132 this.ostentatiousMaster = "light"
133
134 // les conversations, une conversation est un objet possédant les attributs suivants :
135 // - root (entier)
136 // - page (entier)
137 // - reduit (bool)
138 this.conversations = []
139 }
140
141 Client.prototype.setCss = function(css)
142 {
143 if (this.css == css || css == "")
144 return
145
146 this.css = css
147 $("link#cssPrincipale").attr("href", this.css)
148 if (this.autoflush) this.flush(true)
149 }
150
151 Client.prototype.pageSuivante = function(numConv)
152 {
153 if (numConv < 0 && this.pagePrincipale > 1)
154 this.pagePrincipale -= 1
155 else if (this.conversations[numConv].page > 1)
156 this.conversations[numConv].page -= 1
157 }
158
159 Client.prototype.pagePrecedente = function(numConv)
160 {
161 if (numConv < 0)
162 this.pagePrincipale += 1
163 else
164 this.conversations[numConv].page += 1
165 }
166
167 /**
168 * Définit la première page pour la conversation donnée.
169 * @return true si la page a changé sinon false
170 */
171 Client.prototype.goPremierePage = function(numConv)
172 {
173 if (numConv < 0)
174 {
175 if (this.pagePrincipale == 1)
176 return false
177 this.pagePrincipale = 1
178 }
179 else
180 {
181 if (this.conversations[numConv].page == 1)
182 return false
183 this.conversations[numConv].page = 1
184 }
185 return true
186 }
187
188 /**
189 * Ajoute une conversation à la vue de l'utilisateur.
190 * Le profile de l'utilisateur est directement sauvegardé sur le serveur.
191 * @param racines la racine de la conversation (integer)
192 * @return true si la conversation a été créée sinon false (par exemple si la conv existe déjà)
193 */
194 Client.prototype.ajouterConversation = function(racine)
195 {
196 // vérification s'il elle n'existe pas déjà
197 for (var i = 0; i < this.conversations.length; i++)
198 if (this.conversations[i].root == racine)
199 return false
200
201 this.conversations.push({root : racine, page : 1, reduit : false})
202 if (this.autoflush) this.flush(true)
203
204 return true
205 }
206
207 Client.prototype.supprimerConversation = function(num)
208 {
209 if (num < 0 || num >= this.conversations.length) return
210
211 // décalage TODO : supprimer le dernier élément
212 for (var i = num; i < this.conversations.length - 1; i++)
213 this.conversations[i] = this.conversations[i+1]
214 this.conversations.pop()
215
216 if (this.autoflush) this.flush(true)
217 }
218
219 Client.prototype.getJSONLogin = function(login, password)
220 {
221 return {
222 "header" : { "action" : "authentification", "version" : euphorik.conf.versionProtocole },
223 "login" : login,
224 "password" : password
225 }
226 }
227
228 Client.prototype.getJSONLoginCookie = function()
229 {
230 return {
231 "header" : { "action" : "authentification", "version" : euphorik.conf.versionProtocole },
232 "cookie" : this.cookie
233 }
234 }
235
236 /**
237 * le couple (login, password) est facultatif. S'il n'est pas fournit alors il ne sera pas possible
238 * de s'autentifier avec (login, password).
239 */
240 Client.prototype.getJSONEnregistrement = function(login, password)
241 {
242 var mess = { "header" : { "action" : "register", "version" : euphorik.conf.versionProtocole }}
243
244 if (login != undefined && password != undefined)
245 {
246 mess["login"] = login
247 mess["password"] = password
248 }
249
250 return mess;
251 }
252
253 Client.prototype.getJSONConversations = function()
254 {
255 var conversations = new Array()
256 for (var i = 0; i < this.conversations.length; i++)
257 conversations.push({root : this.conversations[i].root, minimized : this.conversations[i].reduit})
258 return conversations
259 }
260
261 Client.prototype.getJSONProfile = function()
262 {
263 return {
264 "header" : { "action" : "set_profile", "version" : euphorik.conf.versionProtocole },
265 "cookie" : this.cookie,
266 "login" : this.login,
267 "password" : this.password,
268 "nick" : this.pseudo,
269 "email" : this.email,
270 "css" : this.css,
271 "chat_order" : this.chatOrder,
272 "nick_format" : this.nickFormat,
273 "view_times" : this.viewTimes,
274 "view_tooltips" : this.viewTooltips,
275 "conversations" : this.getJSONConversations(),
276 "ostentatious_master" : this.ostentatiousMaster
277 }
278 }
279
280 /**
281 * Renvoie null si pas définit.
282 */
283 Client.prototype.getCookie = function()
284 {
285 var cookie = this.regexCookie.exec(document.cookie)
286 if (cookie == null) this.cookie = null
287 else this.cookie = cookie[1]
288 }
289
290 Client.prototype.delCookie = function()
291 {
292 document.cookie = "cookie=; max-age=0"
293 }
294
295 Client.prototype.setCookie = function()
296 {
297 if (this.cookie == null || this.cookie == undefined)
298 return
299
300 // ne fonctionne pas sous IE....
301 /*document.cookie = "cookie=" + this.cookie + "; max-age=" + (60 * 60 * 24 * 365) */
302
303 document.cookie =
304 "cookie="+this.cookie+"; expires=" + new Date(new Date().getTime() + 1000 * 60 * 60 * 24 * 365).toUTCString()
305 }
306
307 Client.prototype.authentifie = function()
308 {
309 return this.statut == statutType.auth_registered || this.statut == statutType.auth_not_registered
310 }
311
312 Client.prototype.setStatut = function(statut)
313 {
314 // conversation en "enum" si en "string"
315 if (typeof(statut) == "string")
316 {
317 statut =
318 statut == "auth_registered" ?
319 statutType.auth_registered :
320 (statut == "auth_not_registered" ? statutType.auth_not_registered : statutType.deconnected)
321 }
322
323 if (statut == this.statut) return
324
325 this.statut = statut
326 this.majMenu()
327 this.majLogo()
328 }
329
330 /**
331 * Effectue la connexion vers le serveur.
332 * Cette fonction est bloquante tant que la connexion n'a pas été établie.
333 * S'il existe un cookie en local on s'authentifie directement avec lui.
334 * Si il n'est pas possible de s'authentifier alors on affiche un captcha anti-bot.
335 */
336 Client.prototype.connexionCookie = function()
337 {
338 this.getCookie()
339 if (this.cookie == null) return false;
340 return this.connexion(this.getJSONLoginCookie())
341 }
342
343 Client.prototype.connexionLogin = function(login, password)
344 {
345 return this.connexion(this.getJSONLogin(login, password))
346 }
347
348 Client.prototype.enregistrement = function(login, password)
349 {
350 if (this.authentifie())
351 {
352 this.login = login
353 this.password = password
354 if(this.flush())
355 {
356 this.setStatut(statutType.auth_registered)
357 return true
358 }
359 return false
360 }
361 else
362 {
363 return this.connexion(this.getJSONEnregistrement(login, password))
364 }
365 }
366
367 /**
368 * Connexion. Réalisé de manière synchrone.
369 */
370 Client.prototype.connexion = function(messageJson)
371 {
372 var thisClient = this
373 jQuery.ajax(
374 {
375 async: false,
376 type: "POST",
377 url: "request",
378 dataType: "json",
379 data: this.util.jsonVersAction(messageJson),
380 success:
381 function(data)
382 {
383 if (data["reply"] == "error")
384 {
385 thisClient.util.messageDialogue(data["error_message"])
386 // suppression du cookie actuel, cas où le cookie du client ne permet pas une authentification
387 thisClient.delCookie()
388 }
389 else
390 thisClient.chargerDonnees(data)
391 }
392 }
393 )
394 return this.authentifie()
395 }
396
397 Client.prototype.deconnexion = function()
398 {
399 this.flush(true)
400 this.delCookie()
401 this.resetDonneesPersonnelles()
402 this.setStatut(statutType.deconnected) // deconnexion
403 }
404
405 Client.prototype.chargerDonnees = function(data)
406 {
407 // la modification du statut qui suit met à jour le menu, le menu dépend (page admin)
408 // de l'état ekMaster
409 this.ekMaster = data["ek_master"] != undefined ? data["ek_master"] : false
410
411 this.setStatut(data["status"])
412
413 if (this.authentifie())
414 {
415 this.cookie = data["cookie"]
416 this.setCookie()
417
418 this.id = data["id"]
419 this.login = data["login"]
420 this.pseudo = data["nick"]
421 this.email = data["email"]
422 this.setCss(data["css"])
423 this.chatOrder = data["chat_order"]
424 this.nickFormat = data["nick_format"]
425 this.viewTimes = data["view_times"]
426 this.viewTooltips = data["view_tooltips"]
427 this.ostentatiousMaster = data["ostentatious_master"]
428
429 // la page de la conversation principale
430 this.pagePrincipale = 1
431
432 // les conversations
433 this.conversations = data["conversations"]
434 for (var i = 0; i < this.conversations.length; i++)
435 this.conversations[i] = {root : this.conversations[i].root, page : 1, reduit : this.conversations[i].minimized}
436
437 this.majBulle()
438 this.majCssSelectionee()
439 //this.majLogo()
440 }
441 }
442
443 /**
444 * Met à jour les données personne sur serveur.
445 * @param async de manière asynchrone ? défaut = true
446 * @return false si le flush n'a pas pû se faire sinon true
447 */
448 Client.prototype.flush = function(async)
449 {
450 if (async == undefined)
451 async = false
452
453 if (!this.authentifie())
454 return false
455
456 var thisClient = this
457 var ok = true
458 jQuery.ajax(
459 {
460 async: async,
461 type: "POST",
462 url: "request",
463 dataType: "json",
464 data: this.util.jsonVersAction(this.getJSONProfile()),
465 success:
466 function(data)
467 {
468 if (data["reply"] == "error")
469 {
470 thisClient.util.messageDialogue(data["error_message"])
471 ok = false
472 }
473 else
474 {
475 thisClient.majBulle()
476 }
477 }
478 }
479 )
480
481 return ok
482 }
483
484 Client.prototype.majMenu = function()
485 {
486 var displayType = "block"
487
488 $("#menu .admin").css("display", this.ekMaster ? displayType : "none")
489
490 // met à jour le menu
491 if (this.statut == statutType.auth_registered)
492 {
493 $("#menu .profile").css("display", displayType).text("profile")
494 $("#menu .logout").css("display", displayType)
495 $("#menu .register").css("display", "none")
496 }
497 else if (this.statut == statutType.auth_not_registered)
498 {
499 $("#menu .profile").css("display", "none")
500 $("#menu .logout").css("display", displayType)
501 $("#menu .register").css("display", displayType)
502 }
503 else
504 {
505 $("#menu .profile").css("display", displayType).text("login")
506 $("#menu .logout").css("display", "none")
507 $("#menu .register").css("display", displayType)
508 }
509 }
510
511 /**
512 * Met à jour l'affichage des infos bulles en fonction du profile.
513 */
514 Client.prototype.majBulle = function()
515 {
516 this.util.bulleActive = this.viewTooltips
517 }
518
519 /**
520 * Met à jour la css sélectionnée, lors du chargement des données.
521 */
522 Client.prototype.majCssSelectionee = function()
523 {
524 // extraction du numéro de la css courante
525 var numCssCourante = this.css.match(/^.*?\/(\d)\/.*$/)
526 if (numCssCourante != null && numCssCourante[1] != undefined)
527 {
528 $("#menuCss option").removeAttr("selected")
529 $("#menuCss option[value=" + numCssCourante[1]+ "]").attr("selected", "selected")
530 }
531 }
532
533 /**
534 * Change la "class" du logo en fonction du statut de ekMaster.
535 */
536 Client.prototype.majLogo = function()
537 {
538 if (this.ekMaster)
539 $("#logo").addClass("ekMaster")
540 else
541 $("#logo").removeClass("ekMaster")
542 }
543
544
545 Client.prototype.slap = function(userId, raison)
546 {
547 var thisClient = this
548
549 jQuery.ajax({
550 type: "POST",
551 url: "request",
552 dataType: "json",
553 data: this.util.jsonVersAction(
554 {
555 "header" : { "action" : "slap", "version" : euphorik.conf.versionProtocole },
556 "cookie" : thisClient.cookie,
557 "user_id" : userId,
558 "reason" : raison
559 }),
560 success:
561 function(data)
562 {
563 if (data["reply"] == "error")
564 thisClient.util.messageDialogue(data["error_message"])
565 }
566 })
567 }
568
569 Client.prototype.ban = function(userId, raison, minutes)
570 {
571 var thisClient = this
572
573 // par défaut un ban correspond à 3 jours
574 if (typeof(minutes) == "undefined")
575 minutes = euphorik.conf.tempsBan;
576
577 jQuery.ajax({
578 type: "POST",
579 url: "request",
580 dataType: "json",
581 data: this.util.jsonVersAction(
582 {
583 "header" : { "action" : "ban", "version" : euphorik.conf.versionProtocole },
584 "cookie" : thisClient.cookie,
585 "duration" : minutes,
586 "user_id" : userId,
587 "reason" : raison
588 }),
589 success:
590 function(data)
591 {
592 if (data["reply"] == "error")
593 thisClient.util.messageDialogue(data["error_message"])
594 }
595 })
596 }
597
598 Client.prototype.kick = function(userId, raison)
599 {
600 this.ban(userId, raison, euphorik.conf.tempsKick)
601 }
602
603 ///////////////////////////////////////////////////////////////////////////////////////////////////
604
605 /**
606 * classe permettant de gérer les événements (push serveur).
607 * l'information envoyé est sous la forme :
608 * {
609 * "header" : {"action" : "wait_event", "version" : <v> },
610 * "page" : <page>
611 * [..]
612 * }
613 * l'information reçu est sous la forme :
614 * {
615 * "reply" : <reply>
616 * }
617 * @page la page courante pour laquelle on écoute des événements
618 * @util le helper 'util'
619 */
620 function PageEvent(page, util) {
621 this.page = page
622 this.util = util
623
624 // l'objet JSONHttpRequest représentant la connexion d'attente
625 this.attenteCourante = null
626
627 // le multhreading du pauvre, merci javascript de m'offrire autant de primitives pour la gestion de la concurrence...
628 this.stop = false
629 }
630
631 /**
632 * Arrête l'attente courante s'il y en a une.
633 */
634 PageEvent.prototype.stopAttenteCourante = function() {
635 this.stop = true
636
637 if (this.attenteCourante != null) {
638 this.attenteCourante.abort()
639 }
640 }
641
642 /**
643 * Attend un événement lié à la page.
644 * @funSend une fonction renvoyant les données json à envoyer
645 * @funsReceive est un objet comprenant les fonctions à appeler en fonction du "reply"
646 * les fonctions acceptent un paramètre correspondant au données reçues.
647 * exemple : {"new_message" : function(data){ ... }}
648 */
649 PageEvent.prototype.waitEvent = function(funSend, funsReceive) {
650 this.stopAttenteCourante()
651
652 this.stop = false
653
654 var thisPageEvent = this
655
656 // on doit conserver l'ordre des valeurs de l'objet JSON (le serveur les veut dans l'ordre définit dans le protocole)
657 // TODO : ya pas mieux ?
658 var dataToSend = {
659 "header" : { "action" : "wait_event", "version" : euphorik.conf.versionProtocole },
660 "page" : this.page
661 }
662 var poulpe = funSend()
663 for (var v in poulpe) {
664 dataToSend[v] = poulpe[v]
665 }
666
667 this.attenteCourante = jQuery.ajax({
668 type: "POST",
669 url: "request",
670 dataType: "json",
671 // TODO : doit disparaitre
672 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
673 data: this.util.jsonVersAction(dataToSend),
674 success:
675 function(data) {
676 funsReceive[data["reply"]](data)
677
678 // rappel de la fonction dans 100 ms
679 setTimeout(function(){ thisPageEvent.waitEvent2(funSend, funsReceive) }, 100)
680 },
681 error:
682 function(XMLHttpRequest, textStatus, errorThrown) {
683 ;; console.log("Connexion perdue dans PageEvent.prototype.waitEvent()")
684 setTimeout(function(){ thisPageEvent.waitEvent2(funSend, funsReceive) }, 1000)
685 }
686 });
687 };
688
689 /**
690 * Si un stopAttenteCourante survient un peu n'importe quand il faut imédiatement arreter de boucler.
691 */
692 PageEvent.prototype.waitEvent2 = function(funSend, funsReceive) {
693 if (this.stop) {
694 return;
695 }
696 this.waitEvent(funSend, funsReceive);
697 };
698
699 ///////////////////////////////////////////////////////////////////////////////////////////////////
700
701
702 // le main
703 $(document).ready(
704 function()
705 {
706 var formateur = new euphorik.Formateur()
707 var util = new euphorik.Util(formateur)
708 var client = new Client(util)
709 var pages = new euphorik.Pages()
710
711 // connexion vers le serveur (utilise un cookie qui traine)
712 client.connexionCookie()
713
714 $("#menuCss").change(function(){ client.setCss("styles/" + $("option:selected", this).attr("value") + "/euphorik.css")})
715
716 // FIXME : ne fonctionne pas sous opera
717 // voir : http://dev.jquery.com/ticket/2892#preview
718 $(window).unload(function(){client.flush()})
719
720 $("#menu .minichat").click(function(){ pages.afficherPage("minichat") })
721 $("#menu .admin").click(function(){ pages.afficherPage("admin") })
722 $("#menu .profile").click(function(){ pages.afficherPage("profile") })
723 $("#menu .logout").click(function(){
724 util.messageDialogue("Êtes-vous sur de vouloir vous délogger ?", euphorik.Util.messageType.question,
725 {"Oui" : function()
726 {
727 client.deconnexion();
728 pages.afficherPage("minichat", true)
729 },
730 "Non" : function(){}
731 }
732 )
733 })
734 $("#menu .register").click(function(){ pages.afficherPage("register") })
735 $("#menu .about").click(function(){ pages.afficherPage("about") })
736
737 // TODO : simplifier et pouvoir créer des liens par exemple : <span class="lien" href="conditions">Conditions d'utilisation</span>
738 $("#footer .conditions").click(function(){ pages.afficherPage("conditions_utilisation") })
739
740 pages.ajouterPage(new PageMinichat(client, formateur, util))
741 pages.ajouterPage(new PageAdmin(client, formateur, util))
742 pages.ajouterPage(new PageProfile(client, formateur, util))
743 pages.ajouterPage(new PageRegister(client, formateur, util))
744 pages.ajouterPage(new PageAbout(client, formateur, util))
745 pages.ajouterPage("conditions_utilisation")
746
747 pages.afficherPage("minichat")
748 }
749 )