From 95caf758476df4a757be18cadfd6c686b91cea17 Mon Sep 17 00:00:00 2001 From: Tom Reynolds Date: Sun, 13 Oct 2013 01:44:27 +0000 Subject: [PATCH] Master server: * more white labeling * installation instructions added * updated database scheme * javascript refresh on HTML server list (thanks Omega!) * desktop notifications on HTML server list (thanks Omega!) --- source/masterserver/README | 21 + source/masterserver/cleanUpServerList.php | 4 - source/masterserver/config.php | 4 +- source/masterserver/functions.php | 4 - source/masterserver/images/game_icon.png | Bin 0 -> 7035 bytes .../masterserver/{ => images}/megaglest.ico | Bin .../scheme_mysql.sql} | 111 ++-- .../scripts/desktop_notifications.js | 316 ++++++++++++ source/masterserver/scripts/json2.js | 486 ++++++++++++++++++ source/masterserver/showRecentServers.php | 4 - source/masterserver/showServers.php | 4 +- source/masterserver/showServersJson.php | 2 +- 12 files changed, 895 insertions(+), 61 deletions(-) create mode 100644 source/masterserver/README create mode 100644 source/masterserver/images/game_icon.png rename source/masterserver/{ => images}/megaglest.ico (100%) rename source/masterserver/{createDB.sql => install/scheme_mysql.sql} (56%) create mode 100644 source/masterserver/scripts/desktop_notifications.js create mode 100644 source/masterserver/scripts/json2.js diff --git a/source/masterserver/README b/source/masterserver/README new file mode 100644 index 00000000..41f21706 --- /dev/null +++ b/source/masterserver/README @@ -0,0 +1,21 @@ +To install: + +1. Setup a MYSQL database server + +2. CREATE DATABASE 'megaglest-master' ENGINE=InnoDB; + CREATE USER 'megaglest-master'@'localhost' IDENTIFIED BY 'secret password'; + GRANT ALL ON 'megaglest-master.*' TO 'megaglest-master'; + +3. Connect the new user to the new database; + Execute the SQL statments in install/scheme_mysql.sql + +4. Copy all files (you can omit INSTALL and install/) to your webserver and + edit config.php to reflect the MySQL connection parameters and game title; + also replace the images in images/ by some which match your game title. + +To test and use this server with your MegaGlest engine based game, configure +the "MasterserverURL" property in glest.ini. + +To add mods to the game mod menu, edit the database contents using your +favorite MySQL editor or develop a web based frontend to do so. In the latter +case, please let us know about it and try to use a compatible license. diff --git a/source/masterserver/cleanUpServerList.php b/source/masterserver/cleanUpServerList.php index f7cd345d..80f61268 100644 --- a/source/masterserver/cleanUpServerList.php +++ b/source/masterserver/cleanUpServerList.php @@ -1,8 +1,4 @@ %G^0uulL;3z4|X}?hEKlU2!rT2tj!C!#lAgv}1 z05m3JJy@Z>_(%|K1sOoyB-P=ILuaF?tp)%DG6MkNQ2@Z*ODOyR0Pq0<07sSpfM^x~ zKQh^D6TJJ#C`o56Gf3hTILl!20at~y;m9>9@5ixf8DL`S9_s=yYq)H@TY-8(tcM# zPrHVJgipS^)MKuw+0PQCG-kCNkkjmc4IVWZ!^V4)JK1D1q!EN@ZPv&ZOiWij5*v7i zVf$1rf$#{k`i{C|;gyr6%loHV=%j}OZCE>Hs@FN$URUIOzrL*s#m>I!d`T>ODlZvd zO*G4-QrCIdyRS11ix74JN~baB%hASstZdoEN8J-MDZC*K^QXUYTmkhgXj$RBp#;<+LB{Z9$3*J=E0+`=>>xekfL zBNuh0JSJ#ZgUgur>Ll#x-;S`@_5Ny&k)n;i%ue~j7fKfrH!YnXc4i}SV4v}v?95EM z{O1!9Y+Qs_NaQW0aDL!L<33t1)f!P5PnY=Rt*X4b-rJV@^grUzx?N4FuzF(BVF`;4+GN`f7uG4bH8+xbzSk0 z)zC1uti^DnZnfU7zh24=ss3hbqh$ceo;GIV40dPe8zOghVfLzzw(H*K`x2s76XcU| z5f1CY)chRE-ys3P@3y=`=vjm#GUs@@JAV|xnYX?%3!F}=9Ii|6Tm>wroRWSM7vwVb zwhGBu^V)Oi0=MRPHlHkt5HsF?S6r{eUYPn6XPzT?t7^hH*T~d^wZ55Y=IOB4YwEXB zaDpw)U{W1>wwb-7I_)XwmzfqHweh7F9 zN$K_;Zq5qWG$&)YSv9(-s@tlZopN6Nkmsv!?lY;3^K@K4m4COZ$L&1bTXQXv9h26D zD#~^EN96T~P^?^aR2^skuFllsIe%}TJYl{eF7x*9h9d_O$mcYecY}(gMY=%tD%D_y zSN+uyY^>x6nXhuM9noS^HKJ4IiSeT=Dl3rQMyF5JB<8AJsZru7;9tZp&DlwJ-Gc(D zMe9?3Qt8bvGa*VjMLm2cKG`@-St0qArMD?q?85fY@~-N*nyNlc-mw2rl_pm-i07f+ zZ}eQzm#rqFQ21sTa`1258Q)>q$}icj{38h3sH5Gfn2+OgQl1=GjjpGO@h(RTZN2MPp zV=Gvs2jcN6(VVZYIk8q)a*uu{F%QOz zP2QKzBQ6^7)(uh&LExq<(-pvBe;} z(GLL)Sw`vBXRq1R97y12e^@%;SJ0*UcV7^o^z+x;*`d_0%!%tJ>M{2|U&lKZ<4DqU zTIphB|JEgXsu~;-^Pl{TE3owNULSuFPd|G%$AB@xNT(!KuIZiY51i`#-7N_Fd*)2r zYu}hcGLYT&Pu8J(civz%F4jFhT_ZbJ#1^$lJ@&$4?T%XCZ&E-0g&6kUipq%%W@D$a zs$M%&!G5KxPR%2VgGqwYQ*sK}iMX%4=tG7=8H=%rhUKfDK@m?xqK0lIGPtgx({U@7 z0o1AINGfz>)j#YbgCu88u;4yz#xfCf=}Wi}BgPHP3 zWZ%_oDpLO=B+^oFaRxM)j6G=8JZr1ZQH`C1H5Ei<+%TrXe|s>%(~eozu~>Tsr_#u5 zrwyp2Lqd*~SNR9b(&KQ&vKP$eK%tV}RV84vqH*>6Ws$};)*yhkiMw1#iMfsN`~KJ1 zzNYfagO#Qqt1 zY*LTUhROJ9b?{Nf*SoSW!f6fR_C}=G$W^8?^iWhVAdS@;w4*>z`jtA+za2L5!`2;< zv}VS*!QRZVOcoBk=AixScF|MABt1_~%25YdgmH1y<50A&$8LiZ`1GyB-L~2p(pw}Z z6g!Ia%cN{dS(qrl%z#~U^?l;Yv~3Rc!?uD)V<(Z~`8@wUldqVTt{K^PG7sY`Zc(G_VM?MuI+xb@? z5Tk#RV;&M;G~c(*n=fb-rYhJ4Hx5qweAOK(W`O;g8 zs!`WV*kkHs)n{|*v>5Wr^So0kZP~tpwc^-FU{9ah15(hYCAZbuO!)pZmTs3W($g2=QaP>f2?HM04(1BSjTi-S}(in>+h|Knz&P z*0IjjD~lWs>Uuw%?9)DY!GV&Wx+7-yRTD#DIvPIpTLIv^S!gl_(UkJx%5sSKezXZ#PL~T#Jx5N73U{94Ymh)+J`vit71>+Fd2uZ=_!q zSo@|G)|~JSV%8JrMfm*Axw`Tafo{BpHOb+O$LC(GT4I-29iVKoS`L|FZE-DRHbR<&3BXbp^f|${`ycgOf`mSL zD6e@sbsgHeeg-YOQw;69^V(e8k|-`?pHx@Y|F{+%KGK5@y5sKu5}C?%9PGv7)*f?+ zjaDlfOpD|x>Fc||4-Wl#C?rgu@6ys;B{)l(%dO$o<1+R5kHw-;<#cQ*%)FymAp$n$ z3js-OdBl;#+Y!jxgzBOQU;lBt;`ibf(id87@|WYQYsGwDpCQBmLM>&J)0K%ul8In% za*5!ZvQ->Sn{p6(R%1YV$LG2^)6vyfi|#d*qdFm=5u8EV(+DByl%8@~w>BHb%AEHV ztIXl$l6k&gfB#!bx_=^htKI0$L@iq(-7>yPjuyM)<}Ax5D^=8*tWhH8pcazTJAywx z(mqjBnW?)6xXDXF>KV%MUui{22D7CFo*t(J3wHc_x7z?S?>;nLMnADFmcu2)3*qWy z+}_GnS21&)2AiO1Idm|2VJs3pW_+(D=%>4@NXVS#sja4&Sri(@BgUH^ zF)cc{@L)*bDY=L0)@M({w!KLB5QTLCB`ouozu_JG_{{RXR1m&o*owbHPwhty@Yu}L zq+v$S(TayyH~+L2MSe%RmTg?+f~tACi7K!e&HGg`9br@3ml%uByj?lq{8#C^-C1mo z<ZGZ{6Ji5HMyjqDJ$)4>3>5<(( zt>M*=NqV@^&b;+pSkfAo54wDqHBE1#n2g^YQ8GD@gU)Z&jVOPjnVO1;F}G#>`F#>?hKqaX)KWBh znfmrHfpClworQQ#V!4z6s?eV0jXE>x%?YCeeG2p|K)ghU)yc}bP{_B@Igm|+!3jsI zpT^biA6tZF2F`Nwb>mq3&jpMy(-47|pN3;>{T9$fewYMLR=opiV@8pac?R)rDh3f2 z>NE(ad=W29jZyq7MWm3)5C|N~pQhB^&~&#^ccf(YNM35G@O#4ddsDF(tx_p&uqUsl zQD@fJWi}6t4MR39-Mg~nx92GJVpQCu{YQ1D;fy$154P(+=|Pu=gY=8ZF$Yt+d#%`I zo+j3vhM1IzJ?|~{n5!oNw|n&|!T07Z7*mYES)POMq06Vnzj5`~2Qaq1a&{hp@07KXZgpwe z%RUIi{n1yCsINj2Os-d)nIJFKg=b>p^~^#}e?@ED^%U!zwy)y(4SRS^*?dQF0Oap> z2M1gQmTG+OVA;#&m2`W#lT>!)I;Ae0GYm6qO%D6&pz=9m!K$Qh%O`YfHjmjUWjSkF z!&baKiOO9iBq)JYK%n?^q$xX2 zygfD@R&@sQpErzZMonbeg0dYl^PgN*s0^@3jz8r(=1&4^3Y{t9Z>dUTXy{M`{m46s zef~sM;iv-@h{5Yn4X}lBBHmfZ`PU%pi$%8qYd3VsYiG^ju)@*7C9>@|bX$djBOjN{(4c`UJKG=^=VdtOW3y_w|TdY_>EI zTLnM^@;R@c#v^v>9d-wwo!jWA&5KMK%z3Acx^$%yMtRAwDyONimZSkeIN0tVda7vb zGJHJJQHgTw#q8LgmAy-DZX!w5zZrlucqHzBN&vz`rX-`s1oVJ^o3Az`@VhnCi6vWLVpEOoM?TxH-N+Xw~YomIJ-Y%p{81dsKflgcZ6o;hyQpN#Tl#2 zv_}p%osf0%ic1>x#|e_S*uV?zh^Pb}HcXO^Wb4d&ndD6PLBeXKANtt2{ZiE1)7=`) zbpR@H4AL~wAV8ykAmBN;L_~#WIJW91OU~M@oR?mI&5t&*vCfMC>6W7$OtykZX6U;) zY))Tv@!t2}UuA5{sc7rXBM-U+LfA#`_IjZ-lvp5D0pI6qp)<-bP$!b8jlH$a);NjT zFqg_T6j*|oDF^MGAr-_tW!ZpAv3E~(}EK7wJ;K?pCE7^QKr%8 zq+=A;_vUYtXD)ZNM~w!$`=yURMMVMH+HX;gd5=iWgr&vF{$4*E!6^cBcU(-U$kK|F zh5{864^~UvI30?nG*VR7YkJ#9J#*xL?)jVTWALmtUu1cnmJZXe@sx%$O4h zV*7_brR?q}U9D^lj=-~Q#zs#OwGEp3NA%=vW6adw;Q1#B*P2?VMSUG@%e1 zD^!{v9C}BQskn#A!=Bs-jZT+b{U15aX$<8r|Iw8-4&M)_jmh_<{qET5kUdKVovj?x zs|1)gh}7+(oI{FWjp4+w+`1}OTWvZZ-3pZlI{Ox`o@|R~Q`BabF{g|A@Q{Vr#q~A4 zMnG#fVi+%sPJLqlwc=Lc>yaB2OxBZYhgnM`-Bb4k?_fQ%SH+oD(EMM`?OxTww2(3a zDsZ^qq-(zS)fNIw6X!gtkXB;R8I#LOj#mMzAXLe`UQvzHD;aG6I)%xH9M0>&u*&~k ze4CHZtt2=+crC{m?N5_+AReBnLMp>2AU!76J56E8h~E8OYNzd!a*ARt&%QB67ryj$ zpYu>kGhsmk<}Y`eVC&88soKCJ-QYV14a>8Oo=O|x(@-_NMIDgqoB+n&5(V#v*F@E{?6mOPe6Qjcn}S2Mh5P zd4~;hx^ya=xFey#fgerat)!dnIJVPn!pS}zKViL9?7{a0O^UCvD>yG=FU$ZPtJVBI;Br;KXzY1;ot|Q zwr;b?jmK}a_uagaR&VtS9be6t5!1@(8r{4l%!WX_a`5m4SkkxA2mC#imbQH&vbjoI zeu{m>ybnQY?++CL2iVTYS8_`6eI;#uT)Dv>^7gyeJdeAjjIIv=uhc}&+9t(^Tooh{ zcVC@25qD-V#1F>K$uYjqU20!Nv@FZ~H86uv?e4jY$8IG-477+ID;%2*R&r?!^Jtdf z=4K5`Iz(z_(?kIll6k?5;!8f(cYTr=>u&6b0_J+8LZS_yTYzHAz3lp>dA3mhxX)8 zkH$to6S_JfmXW|W{`B7UFtLP;gmJ0M+h>a!CV(QT_W>F=5ploa{gpT>GVQ4r2H>ur z=HHFmR3nIejE&18m658NLSK6Da1!sv+ws3un*Y;n!t9=r9AX8i&fR-(UIeAL-e5y- zTPtrnQ5!G27XbKxynI|hUM^k{U0z;M0gxzAfCC5=1p=F~M)v-zz}4N>$v)u!FX+?v yFn%c@{m%*>ZVsZ>PTpL$b{^i2Tz+0o-rja@fY;>%oPS@Y0F=P$a&
" + + " "; +wrapperDiv.style.paddingLeft = "30px"; + +domBody[0].insertBefore(wrapperDiv, domUl[0]); +domUl[0].parentNode.removeChild(domUl[0]); + + +// Request permission for issuing desktop notifications when the checkbox is ticked +var notifications = document.getElementById("enableNotifications"); +notifications.onclick = function() +{ + if(notifications.checked) + { + Notification.requestPermission(); + } +} + + +// Helper function for escpaing special characters +function escapeHtml(text) { + return text.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'"); +} + + +// Check the JSON data for changes at intervals and update table +function timedRequest() +{ + // Break out if the checkbox isn't ticked + if(!notifications.checked) + { + return; + } + + // Get JSON of server list + var request = new XMLHttpRequest(); + request.open('GET', 'showServersJson.php', true); + request.send(); + + // Function calls as soon as we receive the right data from the site + request.onreadystatechange = function() + { + if (request.readyState == 4 && request.status == 200) + { + // Parse the JSON data for safety + var jsonText = JSON.parse(request.responseText); + var newServerList = {}; + + // Repopulate table content + var table = "\n" + + " Version\n" + + " Status\n" + + " Country\n" + + " Title\n" + + " Techtree\n" + + " Network players\n" + + " Network slots\n" + + " Total slots\n" + + " Map\n" + + " Tileset\n" + + " IPv4 address\n" + + " Game protocol port\n" + + " Platform\n" + + " Build date\n" + + "\n"; + + // Loop through all json objects + for(var i = 0; i < jsonText.length; i++) + { + // Check if version filter is active + if(version == '' || jsonText[i].glestVersion == version) + { + ////// DYNAMIC TABLE SECTION + + table += ""; + + /// Version + table += "" + escapeHtml(jsonText[i].glestVersion) + ""; + + /// Status + var statusCode = jsonText[i].status; + + // Change text if the server is full + if((statusCode == 0) && (jsonText[i].networkSlots <= jsonText[i].connectedClients)) + { + statusCode = 1; + } + var statusTitle, statusClass; + // Note that the json value is stored as a string, not a number + switch(statusCode) + { + case "0": + statusTitle = 'waiting for players'; + statusClass = 'waiting_for_players'; + break; + case "1": + statusTitle = 'game full, pending start'; + statusClass = 'game_full_pending_start'; + break; + case "2": + statusTitle = 'in progress'; + statusClass = 'in_progress'; + break; + case "3": + statusTitle = 'finished'; + statusClass = 'finished'; + break; + default: + statusTitle = 'unknown'; + statusClass = 'unknown'; + } + + table += "" + escapeHtml(statusTitle) + ""; + + /// Country + if(jsonText[i].country !== "") + { + var flagFile = "flags/" + jsonText[i].country.toLowerCase() + ".png"; + table += "\"""; + } + else + { + table += "Unknown"; + } + + /// Server title + table += "" + escapeHtml(jsonText[i].serverTitle) + ""; + + /// Tech + table += "" + escapeHtml(jsonText[i].tech) + ""; + + /// Connected clients + table += "" + escapeHtml(jsonText[i].connectedClients) + ""; + + /// Network slots + table += "" + escapeHtml(jsonText[i].networkSlots) + ""; + + /// Active slots + table += "" + escapeHtml(jsonText[i].activeSlots) + ""; + + /// Map + table += "" + escapeHtml(jsonText[i].map) + ""; + + /// Tileset + table += "" + escapeHtml(jsonText[i].tileset) + ""; + + /// IP + table += "" + escapeHtml(jsonText[i].ip) + ""; + + /// Port + table += "" + escapeHtml(jsonText[i].externalServerPort) + ""; + + /// Platform + table += "" + escapeHtml(jsonText[i].platform) + ""; + + /// Binary compilation date + table += "" + escapeHtml(jsonText[i].binaryCompileDate) + ""; + + table += ""; + + ////// DESKTOP NOTIFICATIONS SECTION + + // Store data in an array keyed by the concatenation of the IP and port + var identifier = jsonText[i].ip + jsonText[i].externalServerPort; + newServerList[identifier] = { 'ip': jsonText[i].ip, 'port': jsonText[i].externalServerPort, 'title': jsonText[i].serverTitle, 'free': (jsonText[i].networkSlots - jsonText[i].connectedClients), 'version': jsonText[i].glestVersion }; + + // Only check for changes if NOT the first time + if(!firstLoop) + { + // Check if new server doesn't exist in old list + if((newServerList[identifier].free > 0) && !serverList[identifier] && statusCode == 0) + { + // Create notification + var notification = new Notification("Open server", { + iconUrl: 'images/game_icon.png', + body: 'Server "' + newServerList[identifier].title + '" has ' + newServerList[identifier].free + ' free slots available. Click to join now.', + }); + + notification.onclick = function() { window.location.assign('http://play.mg/?version=' + newServerList[identifier].version + '&mgg_host=' + newServerList[identifier].ip + '&mgg_port=' + newServerList[identifier].port); }; + } + } + else + { + firstLoop = false; + } + } + } + // Replace old list with new one + serverList = newServerList; + + // Write to actual table when done only, otherwise the browser trips as it tries to fix the partial table formatting + var tableDOM = document.getElementsByTagName("tbody"); + tableDOM[0].innerHTML = table; + + // Catch empty case + if(jsonText.length == 0) + { + serverList = { }; + } + } + // Empty server list + else if(request.readyState == 4 && request.status == 0) + { + serverList = { }; + } + } +} + + +// Default time in miliseconds between updates +var refreshTime = DEFAULT_REFRESH_TIME; + +// Check if there's an HTTP refresh query. If so, we need to overwrite it +if(get_data['refresh']) +{ + // Get the base URL without any GET parameters (because we have to remove the + // old refresh variable) + var redirectLocation = location.href.split("?")[0] + "?"; + + // If a version variable was specified, add that back in + if(get_data['version']) + { + redirectLocation += "version=" + get_data['version'] + "&"; + } + + // Finally the new refresh variable just for JS use + redirectLocation += "jsrefresh=" + get_data['refresh']; + + window.location.replace(redirectLocation); +} + +// Check if there's a js refresh query +if(get_data['jsrefresh']) +{ + // In seconds, so multiply by 1000 for miliseconds + refreshTime = parseInt(get_data['jsrefresh']) * 1000; +} + +// Initialize value in text field +var refreshTimeBox = document.getElementById("refreshTimeId"); +refreshTimeBox.value = refreshTime / 1000; + + +// Initiate interval +timedRequest(); +var interval = setInterval(timedRequest, refreshTime); + + +// Catch changes to the refresh time box +refreshTimeBox.onchange = function() +{ + // Validate if the input is a number + if(!isNaN(parseFloat(refreshTimeBox.value)) && isFinite(refreshTimeBox.value)) + { + if(refreshTimeBox.value < 10) + { + refreshTime = 10000; + refreshTimeBox.value = 10; + } + else if(refreshTimeBox.value > 999) + { + refreshTime = 999000; + refreshTimeBox.value = 999; + } + else + { + refreshTime = refreshTimeBox.value * 1000; + } + } + else + { + refreshTime = DEFAULT_REFRESH_TIME; + refreshTimeBox.value = 20; + } + + // Reset the interval + clearInterval(interval); + interval = setInterval(timedRequest, refreshTime); +} diff --git a/source/masterserver/scripts/json2.js b/source/masterserver/scripts/json2.js new file mode 100644 index 00000000..d89ecc7a --- /dev/null +++ b/source/masterserver/scripts/json2.js @@ -0,0 +1,486 @@ +/* + json2.js + 2013-05-26 + + Public Domain. + + NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + + See http://www.JSON.org/js.html + + + This code should be minified before deployment. + See http://javascript.crockford.com/jsmin.html + + USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO + NOT CONTROL. + + + This file creates a global JSON object containing two methods: stringify + and parse. + + JSON.stringify(value, replacer, space) + value any JavaScript value, usually an object or array. + + replacer an optional parameter that determines how object + values are stringified for objects. It can be a + function or an array of strings. + + space an optional parameter that specifies the indentation + of nested structures. If it is omitted, the text will + be packed without extra whitespace. If it is a number, + it will specify the number of spaces to indent at each + level. If it is a string (such as '\t' or ' '), + it contains the characters used to indent at each level. + + This method produces a JSON text from a JavaScript value. + + When an object value is found, if the object contains a toJSON + method, its toJSON method will be called and the result will be + stringified. A toJSON method does not serialize: it returns the + value represented by the name/value pair that should be serialized, + or undefined if nothing should be serialized. The toJSON method + will be passed the key associated with the value, and this will be + bound to the value + + For example, this would serialize Dates as ISO strings. + + Date.prototype.toJSON = function (key) { + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + return this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z'; + }; + + You can provide an optional replacer method. It will be passed the + key and value of each member, with this bound to the containing + object. The value that is returned from your method will be + serialized. If your method returns undefined, then the member will + be excluded from the serialization. + + If the replacer parameter is an array of strings, then it will be + used to select the members to be serialized. It filters the results + such that only members with keys listed in the replacer array are + stringified. + + Values that do not have JSON representations, such as undefined or + functions, will not be serialized. Such values in objects will be + dropped; in arrays they will be replaced with null. You can use + a replacer function to replace those with JSON values. + JSON.stringify(undefined) returns undefined. + + The optional space parameter produces a stringification of the + value that is filled with line breaks and indentation to make it + easier to read. + + If the space parameter is a non-empty string, then that string will + be used for indentation. If the space parameter is a number, then + the indentation will be that many spaces. + + Example: + + text = JSON.stringify(['e', {pluribus: 'unum'}]); + // text is '["e",{"pluribus":"unum"}]' + + + text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); + // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' + + text = JSON.stringify([new Date()], function (key, value) { + return this[key] instanceof Date ? + 'Date(' + this[key] + ')' : value; + }); + // text is '["Date(---current time---)"]' + + + JSON.parse(text, reviver) + This method parses a JSON text to produce an object or array. + It can throw a SyntaxError exception. + + The optional reviver parameter is a function that can filter and + transform the results. It receives each of the keys and values, + and its return value is used instead of the original value. + If it returns what it received, then the structure is not modified. + If it returns undefined then the member is deleted. + + Example: + + // Parse the text. Values that look like ISO date strings will + // be converted to Date objects. + + myData = JSON.parse(text, function (key, value) { + var a; + if (typeof value === 'string') { + a = +/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); + if (a) { + return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], + +a[5], +a[6])); + } + } + return value; + }); + + myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { + var d; + if (typeof value === 'string' && + value.slice(0, 5) === 'Date(' && + value.slice(-1) === ')') { + d = new Date(value.slice(5, -1)); + if (d) { + return d; + } + } + return value; + }); + + + This is a reference implementation. You are free to copy, modify, or + redistribute. +*/ + +/*jslint evil: true, regexp: true */ + +/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, + call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, + getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, + lastIndex, length, parse, prototype, push, replace, slice, stringify, + test, toJSON, toString, valueOf +*/ + + +// Create a JSON object only if one does not already exist. We create the +// methods in a closure to avoid creating global variables. + +if (typeof JSON !== 'object') { + JSON = {}; +} + +(function () { + 'use strict'; + + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + if (typeof Date.prototype.toJSON !== 'function') { + + Date.prototype.toJSON = function () { + + return isFinite(this.valueOf()) + ? this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z' + : null; + }; + + String.prototype.toJSON = + Number.prototype.toJSON = + Boolean.prototype.toJSON = function () { + return this.valueOf(); + }; + } + + var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + gap, + indent, + meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }, + rep; + + + function quote(string) { + +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can safely slap some quotes around it. +// Otherwise we must also replace the offending characters with safe escape +// sequences. + + escapable.lastIndex = 0; + return escapable.test(string) ? '"' + string.replace(escapable, function (a) { + var c = meta[a]; + return typeof c === 'string' + ? c + : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : '"' + string + '"'; + } + + + function str(key, holder) { + +// Produce a string from holder[key]. + + var i, // The loop counter. + k, // The member key. + v, // The member value. + length, + mind = gap, + partial, + value = holder[key]; + +// If the value has a toJSON method, call it to obtain a replacement value. + + if (value && typeof value === 'object' && + typeof value.toJSON === 'function') { + value = value.toJSON(key); + } + +// If we were called with a replacer function, then call the replacer to +// obtain a replacement value. + + if (typeof rep === 'function') { + value = rep.call(holder, key, value); + } + +// What happens next depends on the value's type. + + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + +// JSON numbers must be finite. Encode non-finite numbers as null. + + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + +// If the value is a boolean or null, convert it to a string. Note: +// typeof null does not produce 'null'. The case is included here in +// the remote chance that this gets fixed someday. + + return String(value); + +// If the type is 'object', we might be dealing with an object or an array or +// null. + + case 'object': + +// Due to a specification blunder in ECMAScript, typeof null is 'object', +// so watch out for that case. + + if (!value) { + return 'null'; + } + +// Make an array to hold the partial results of stringifying this object value. + + gap += indent; + partial = []; + +// Is the value an array? + + if (Object.prototype.toString.apply(value) === '[object Array]') { + +// The value is an array. Stringify every element. Use null as a placeholder +// for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + +// Join all of the elements together, separated with commas, and wrap them in +// brackets. + + v = partial.length === 0 + ? '[]' + : gap + ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' + : '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + +// If the replacer is an array, use it to select the members to be stringified. + + if (rep && typeof rep === 'object') { + length = rep.length; + for (i = 0; i < length; i += 1) { + if (typeof rep[i] === 'string') { + k = rep[i]; + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } else { + +// Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } + +// Join all of the member texts together, separated with commas, +// and wrap them in braces. + + v = partial.length === 0 + ? '{}' + : gap + ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' + : '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + } + +// If the JSON object does not yet have a stringify method, give it one. + + if (typeof JSON.stringify !== 'function') { + JSON.stringify = function (value, replacer, space) { + +// The stringify method takes a value and an optional replacer, and an optional +// space parameter, and returns a JSON text. The replacer can be a function +// that can replace values, or an array of strings that will select the keys. +// A default replacer method can be provided. Use of the space parameter can +// produce text that is more easily readable. + + var i; + gap = ''; + indent = ''; + +// If the space parameter is a number, make an indent string containing that +// many spaces. + + if (typeof space === 'number') { + for (i = 0; i < space; i += 1) { + indent += ' '; + } + +// If the space parameter is a string, it will be used as the indent string. + + } else if (typeof space === 'string') { + indent = space; + } + +// If there is a replacer, it must be a function or an array. +// Otherwise, throw an error. + + rep = replacer; + if (replacer && typeof replacer !== 'function' && + (typeof replacer !== 'object' || + typeof replacer.length !== 'number')) { + throw new Error('JSON.stringify'); + } + +// Make a fake root object containing our value under the key of ''. +// Return the result of stringifying the value. + + return str('', {'': value}); + }; + } + + +// If the JSON object does not yet have a parse method, give it one. + + if (typeof JSON.parse !== 'function') { + JSON.parse = function (text, reviver) { + +// The parse method takes a text and an optional reviver function, and returns +// a JavaScript value if the text is a valid JSON text. + + var j; + + function walk(holder, key) { + +// The walk method is used to recursively walk the resulting structure so +// that modifications can be made. + + var k, v, value = holder[key]; + if (value && typeof value === 'object') { + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + } + + +// Parsing happens in four stages. In the first stage, we replace certain +// Unicode characters with escape sequences. JavaScript handles many characters +// incorrectly, either silently deleting them, or treating them as line endings. + + text = String(text); + cx.lastIndex = 0; + if (cx.test(text)) { + text = text.replace(cx, function (a) { + return '\\u' + + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }); + } + +// In the second stage, we run the text against regular expressions that look +// for non-JSON patterns. We are especially concerned with '()' and 'new' +// because they can cause invocation, and '=' because it can cause mutation. +// But just to be safe, we want to reject all unexpected forms. + +// We split the second stage into 4 regexp operations in order to work around +// crippling inefficiencies in IE's and Safari's regexp engines. First we +// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we +// replace all simple value tokens with ']' characters. Third, we delete all +// open brackets that follow a colon or comma or that begin the text. Finally, +// we look to see that the remaining characters are only whitespace or ']' or +// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + if (/^[\],:{}\s]*$/ + .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') + .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') + .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + +// In the third stage we use the eval function to compile the text into a +// JavaScript structure. The '{' operator is subject to a syntactic ambiguity +// in JavaScript: it can begin a block or an object literal. We wrap the text +// in parens to eliminate the ambiguity. + + j = eval('(' + text + ')'); + +// In the optional fourth stage, we recursively walk the new structure, passing +// each name/value pair to a reviver function for possible transformation. + + return typeof reviver === 'function' + ? walk({'': j}, '') + : j; + } + +// If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError('JSON.parse'); + }; + } +}()); diff --git a/source/masterserver/showRecentServers.php b/source/masterserver/showRecentServers.php index fcdde2e5..fd6f41ff 100644 --- a/source/masterserver/showRecentServers.php +++ b/source/masterserver/showRecentServers.php @@ -1,8 +1,4 @@ ' . PHP_EOL; echo ' ' . htmlspecialchars( PRODUCT_NAME ) . ' gameservers' . PHP_EOL; echo ' ' . PHP_EOL; - echo ' ' . PHP_EOL; + echo ' ' . PHP_EOL; echo ' ' . PHP_EOL; echo ' ' . PHP_EOL; echo '

' . htmlspecialchars( PRODUCT_NAME ) . ' gameservers

' . PHP_EOL; @@ -193,6 +193,8 @@ echo '
  • You can have this page auto refresh every 60 seconds by appending ?refresh=60 to the URL. Minimum refresh time is 10 seconds.
  • ' . PHP_EOL; echo '
  • The parameters used by the masterserver API will display when you move your mouse pointer over any of the table headings.
  • ' . PHP_EOL; echo ' ' . PHP_EOL; + echo ' ' . PHP_EOL; + echo ' ' . PHP_EOL; echo ' ' . PHP_EOL; echo '' . PHP_EOL; diff --git a/source/masterserver/showServersJson.php b/source/masterserver/showServersJson.php index f70e1662..81fed491 100644 --- a/source/masterserver/showServersJson.php +++ b/source/masterserver/showServersJson.php @@ -12,7 +12,7 @@ // consider replacing this by a cron job cleanupServerList(); - $servers_in_db = mysql_query( 'SELECT * FROM glestserver ORDER BY status, connectedClients>0 DESC, (networkSlots - connectedClients) , ip DESC;' ); + $servers_in_db = mysql_query( 'SELECT * FROM glestserver ORDER BY status, connectedClients>0 DESC, (networkSlots - connectedClients), ip DESC;' ); $all_servers = array(); while ( $server = mysql_fetch_array( $servers_in_db ) ) {