Sync changes in from Nighthawk
authorDan
Sun, 07 Oct 2007 21:31:14 -0400
changeset 180 c63c5ee6c6d6
parent 179 36b287f1d85c (diff)
parent 152 9b4279c25d33 (current diff)
child 181 06bdbdfec160
Sync changes in from Nighthawk
.hgtags
install.php
schema.sql
upgrade.php
upgrade.sql
--- a/.hgtags	Sun Sep 30 19:22:04 2007 -0400
+++ b/.hgtags	Sun Oct 07 21:31:14 2007 -0400
@@ -3,3 +3,6 @@
 8df3abef66473fef1a8ecbbd60a4e94e4f466ec4 release
 ca9118d9c0f2be22407860f41523f47b2862b34a rebrand
 6f0bbf88c3251ca597cb76ac8b59a1ee61d6dd3d rebrand
+0b5244001799fa29e83bf06c5f14eb69350f171c rebrand
+42c6c83b8a004163c9cc2d85f3c8eada3b73adf6 rebrand
+d53cc29308f4f4b97fc6d054e9e0855f37137409 rebrand
--- a/ajax.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/ajax.php	Sun Oct 07 21:31:14 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  *
  * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
--- a/includes/captcha.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/includes/captcha.php	Sun Oct 07 21:31:14 2007 -0400
@@ -1,7 +1,8 @@
 <?php
+
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  * captcha.php - visual confirmation system used during registration
  *
--- a/includes/clientside/jsres.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/includes/clientside/jsres.php	Sun Oct 07 21:31:14 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  * jsres.php - the Enano client-side runtime, a.k.a. AJAX on steroids
  *
@@ -40,7 +40,7 @@
 {
   echo "/*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * [Aggressively compressed] Javascript client code
  * Copyright (C) 2006-2007 Dan Fuhry
  * Enano is Free Software, licensed under the GNU General Public License; see http://enanocms.org/ for details.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/includes/clientside/static/SpryEffects.js	Sun Oct 07 21:31:14 2007 -0400
@@ -0,0 +1,29 @@
+// Spry.Effect.js - version 0.38 - Spry Pre-Release 1.6
+//
+// Copyright (c) 2007. Adobe Systems Incorporated.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+//   * Redistributions of source code must retain the above copyright notice,
+//     this list of conditions and the following disclaimer.
+//   * Redistributions in binary form must reproduce the above copyright notice,
+//     this list of conditions and the following disclaimer in the documentation
+//     and/or other materials provided with the distribution.
+//   * Neither the name of Adobe Systems Incorporated nor the names of its
+//     contributors may be used to endorse or promote products derived from this
+//     software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+eval(function(p,a,c,k,e,d){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('h b;j(!b)b={};b.15=1;b.1J=2;j(!b.c)b.c={};b.c.4Z={43:p(19,1c,1e,r){j(19>r)q 1e+1c;q 1c+(19/r)*1e},4q:p(19,1c,1e,r){j(19>r)q 1e+1c;q 1c+((-1g.5W((19/r)*1g.5V)/2)+0.5)*1e},6a:p(19,1c,1e,r){j(19>r)q 1e+1c;q 1c+1g.4C(19/r,2)*1e},7G:p(19,1c,1e,r){j(19>r)q 1e+1c;q 1c+1g.50(19/r)*1e},3I:p(19,1c,1e,r){j(19>r)q 1e+1c;q 1c+1g.50((-1g.5W((19/r)*1g.5V)/2)+0.5)*1e},5Z:p(19,1c,1e,r){j(19>r)q 1e+1c;h 1F=19/r;q 1c+1g.50(1-1g.4C((1F-1),2))*1e},7J:p(19,1c,1e,r){j(19>r)q 1e+1c;q 1c+(0.5+1g.7E(17*19/r)/2)*1e},7D:p(19,1c,1e,r){j(19>r)q 1e+1c;h 1F=19/r;q 1c+(5*1g.4C(1F,3)-6.4*1g.4C(1F,2)+2*1F)*1e}};18(h 51 3K b.c.4Z){b[51]=b.c.4Z[51]}b.c.3V=p(){8.2R=[]};b.c.3V.w.4W=p(k,d){h a={};a.k=b.c.1x(k);a.d=d;18(h i=0;i<8.2R.M;i++)j(8.5U(8.2R[i],a))q 8.2R[i].J;q D};b.c.3V.w.6e=p(J,k,d){j(!8.4W(k,d)){h 2C=8.2R.M;8.2R[2C]={};h 4F=8.2R[2C];4F.J=J;4F.k=b.c.1x(k);4F.d=d}};b.c.3V.w.5U=p(3U,32){j(3U.k!=32.k)q D;h 52=b.c.m.6b(3U.d,32.d);j(52){j(1n 32.d.3A==\'p\')3U.d.3A=32.d.3A;j(1n 32.d.3o==\'p\')3U.d.3o=32.d.3o}q 52};h 4R=B b.c.3V;j(!b.c.m)b.c.m={};b.c.m.1D=p(61){7V(\'b.c 7W: \'+61)};b.c.m.21=p(J){b.c.m.1D(\'7S \'+J+\' 7R 7N\\\'t 7M 7w 7P a 5o p 7Q. \'+"\\n"+\'7Y 7r b 7c 7f 7e.\');q D};b.c.m.1G=p(){8.x=0;8.y=0;8.N="1Q"};b.c.m.2d=p(){8.A=0;8.C=0;8.N="1Q"};b.c.m.3m=p(5R){h 1H=5R.7h(16);j(1H.M==1)1H="0"+1H;q 1H};b.c.m.2e=p(64){q Y(64,16)};b.c.m.3j=p(5Q,5J,5I){h 3m=b.c.m.3m;h 5L=3m(5Q);h 5O=3m(5J);h 5N=3m(5I);4i=5L.7a(5O,5N).6i();4i=\'#\'+4i;q 4i};b.c.m.5k=p(2K){j(2K.7j(/^#[0-7s-f]{3}$/i)){h 3x=2K.6l(\'\');h 2K=\'#\';18(h i=1;i<3x.M;i++){2K+=3x[i]+\'\'+3x[i]}}q 2K};b.c.m.5f=p(4k){j(4k.25(\'-\')==-1){q 4k}h 44=4k.6l(\'-\');h 53=O;h 4y=\'\';18(h i=0;i<44.M;i++){j(44[i].M>0){j(53){4y=44[i];53=D}H{h s=44[i];4y+=s.7q(0).6i()+s.2s(1)}}}q 4y};b.c.m.1N=p(1i){h 1H=D;j(1n 1i==\'58\'&&1i.M>0&&1i.33("%")>0)1H=O;q 1H};b.c.m.1k=p(1i){h 1H=0;38{1H=4a(1i.2s(0,1i.33("%")))}37(e){b.c.m.1D(\'b.c.m.1k: \'+e)}q 1H};b.c.m.2A=p(1i){h 1H=0;j(1n 1i==\'5T\')q 1i;h 4j=1i.33("1Q");j(4j==-1)4j=1i.M;38{1H=Y(1i.2s(0,4j),10)}37(e){}q 1H};b.c.m.6r=p(2X){j(2X){h 3e=2X.7l;71(3e){j(3e.5b==1)q 3e;3e=3e.7k}}q z};b.c.m.6A=p(3L,4Y){j(!3L||3L.5b!=1||!4Y)q;j(3L.7m()){h 2g=3L.7Z(\'7n\');h 6h=2g.M;18(h i=0;i<6h;i++){h 4X=2g[i];h 4T=b.c.4b(4X);4Y.7O([4X,4T.A,4T.C])}}};b.c.m.6b=p(35,3p){j(35==z&&3p==z)q O;j(35!=z&&3p!=z){h 4S=0;h 4U=0;18(h 8x 3K 35)4S++;18(h 8y 3K 3p)4U++;j(4S!=4U)q D;18(h 1y 3K 35){h 4V=1n 35[1y];h 6f=1n 3p[1y];j(4V!=6f||(4V!=\'3D\'&&35[1y]!=3p[1y]))q D}q O}q D};b.c.m.23=p(54,k,d){j(!d)h d={};d.1t=54;h 40=4R.4W(k,d);j(!40){40=B b.c[54](k,d);4R.6e(40,k,d)}40.2J();q O};j(!b.m)b.m={};b.m.2y=p(){8.2m=[];8.3Y=0};b.m.2y.w.2N=p(36){j(!36)q;h 2C=8.2m.M;18(h i=0;i<2C;i++)j(8.2m[i]==36)q;8.2m[2C]=36};b.m.2y.w.8n=p(36){j(!36)q;18(h i=0;i<8.2m.M;i++){j(8.2m[i]==36){8.2m.88(i,1);3P}}};b.m.2y.w.2t=p(3X,55){j(!3X)q;j(!8.3Y){h 2C=8.2m.M;18(h i=0;i<2C;i++){h 3y=8.2m[i];j(3y){j(1n 3y=="p")3y(3X,8,55);H j(3y[3X])3y[3X](8,55)}}}};b.m.2y.w.69=p(){j(--8.3Y<0){8.3Y=0;b.c.m.1D("82 69() 1s!\\n")}};b.m.2y.w.84=p(){++8.3Y};b.c.1x=p(2Q){h k=2Q;j(1n 2Q=="58")k=3f.70(2Q);j(k==z)b.c.m.1D(\'8c "\'+2Q+\'" 8j 4p.\');q k};b.c.1a=p(k,1y){h 1i;h 5e=b.c.m.5f(1y);38{j(k.11)1i=k.11[5e];j(!1i){j(3f.4J&&3f.4J.2x){h 5d=3f.4J.2x(k,z);1i=5d?5d.8m(1y):z}H j(k.57){1i=k.57[5e]}}}37(e){b.c.m.1D(\'b.c.1a: \'+e)}q 1i==\'8d\'?z:1i};b.c.1h=p(k,1y,1i){38{k.11[b.c.m.5f(1y)]=1i}37(e){b.c.m.1D(\'b.c.1h: \'+e)}};b.c.2Z=p(k,1y,3i){h 1z=3i?3i:k;h 3k=b.c.1a(1z,\'1R\');h 4E=b.c.1a(1z,\'2l\');j(3k==\'1Z\'){b.c.1h(1z,\'2l\',\'3d\');b.c.1h(1z,\'1R\',\'3T\');j(47.56)1z.5M()}h 6q=b.c.1a(k,1y);j(3k==\'1Z\'){b.c.1h(1z,\'1R\',\'1Z\');b.c.1h(1z,\'2l\',4E)}q 6q};b.c.2L=p(k){h 1F=b.c.1a(k,\'1L\');j(!1F||1F==\'5o\'){k.11.1L=\'6g\';j(47.56){k.11.1b=0;k.11.1f=0}}};b.c.5i=p(k){h 3E=b.c.1a(k,\'1R\');j(3E&&3E.3C()==\'1Z\')q O;h 3B=b.c.1a(k,\'2l\');j(3B&&3B.3C()==\'3d\')q O;q D};b.c.2o=p(k){h 3E=b.c.1a(k,\'1R\');j(3E&&3E.3C()==\'1Z\')b.c.1h(k,\'1R\',\'3T\');h 3B=b.c.1a(k,\'2l\');j(3B&&3B.3C()==\'3d\')b.c.1h(k,\'2l\',\'8h\')};b.c.3l=p(k){h 3z=b.c.1a(k,\'3z\');j(!3z||(3z.3C()!=\'3d\'&&3z.3C()!=\'81\')){h 5c=0;h 5g=/2M 7.0/.1j(27.26)&&/5n 5D/.1j(27.26);j(5g)5c=b.c.2n(k).C;b.c.1h(k,\'3z\',\'3d\');j(5g)b.c.1h(k,\'C\',5c+\'1Q\')}};b.c.65=p(k){h 6j=k.6t.M;18(h i=6j-1;i>=0;i--){h 2X=k.6t[i];j(2X.5b==3&&!/\\S/.1j(2X.8I))38{k.8z(2X)}37(e){b.c.m.1D(\'b.c.65: \'+e)}}};b.c.2x=p(k){q/2M/.1j(27.26)?k.57:3f.4J.2x(k,z)};b.c.4b=p(k){h 1T=B b.c.m.2d;h 1d=z;j(k.11.A&&/1Q/i.1j(k.11.A))1T.A=Y(k.11.A,10);H{1d=b.c.2x(k);h 1W=1d&&1d.A&&/1Q/i.1j(1d.A);j(1W)1T.A=Y(1d.A,10);j(!1W||1T.A==0)1T.A=k.8p}j(k.11.C&&/1Q/i.1j(k.11.C))1T.C=Y(k.11.C,10);H{j(!1d)1d=b.c.2x(k);h 1W=1d&&1d.C&&/1Q/i.1j(1d.C);j(1W)1T.C=Y(1d.C,10);j(!1W||1T.C==0)1T.C=k.8t}q 1T};b.c.2n=p(k,3i){h 1z=3i?3i:k;h 3k=b.c.1a(1z,\'1R\');h 4E=b.c.1a(1z,\'2l\');j(3k==\'1Z\'){b.c.1h(1z,\'2l\',\'3d\');b.c.1h(1z,\'1R\',\'3T\');j(47.56)1z.5M()}h 1T=b.c.4b(k);j(3k==\'1Z\'){b.c.1h(1z,\'1R\',\'1Z\');b.c.1h(1z,\'2l\',4E)}q 1T};b.c.5w=p(k){h o=b.c.1a(k,"1E");j(1n o==\'3D\'||o==z)o=1.0;q o};b.c.4G=p(2Q){q b.c.1a(2Q,"4e-2K")};b.c.3W=p(e,1y){h i=Y(b.c.1a(e,1y),10);j(5F(i))q 0;q i};b.c.48=p(k){h 1L=B b.c.m.1G;h 1d=z;j(k.11.1f&&/1Q/i.1j(k.11.1f))1L.x=Y(k.11.1f,10);H{1d=b.c.2x(k);h 1W=1d&&1d.1f&&/1Q/i.1j(1d.1f);j(1W)1L.x=Y(1d.1f,10);j(!1W||1L.x==0)1L.x=k.8g}j(k.11.1b&&/1Q/i.1j(k.11.1b))1L.y=Y(k.11.1b,10);H{j(!1d)1d=b.c.2x(k);h 1W=1d&&1d.1b&&/1Q/i.1j(1d.1b);j(1W)1L.y=Y(1d.1b,10);j(!1W||1L.y==0)1L.y=k.8a}q 1L};b.c.87=b.c.48;b.c.X=p(d){b.m.2y.1s(8);8.1t=\'X\';8.k=z;8.2D=0;8.8E=\'1Z\';8.1u=D;8.49=z;8.2f=0;j(!d)h d={};j(d.F)8.E=D;H 8.E=b.15;h 1q=8;j(d.3A!=z)8.2N({3Q:p(){38{1q.d.3A(1q.k,1q)}37(e){b.c.m.1D(\'b.c.X.w.2J: 3A 5Y: \'+e)}}});j(d.3o!=z)8.2N({3J:p(){38{1q.d.3o(1q.k,1q)}37(e){b.c.m.1D(\'b.c.X.w.29: 3o 5Y: \'+e)}}});8.d={r:1O,F:D,u:b.43,3c:16};8.5X(d);j(d.u)8.59(d.u);j(d.v)8.5a(d.v)};b.c.X.w=B b.m.2y();b.c.X.w.1v=b.m.X;b.c.X.w.1Y=O;b.c.X.w.5X=p(d){j(!d)q;18(h 1y 3K d)8.d[1y]=d[1y]};b.c.X.w.59=p(u){j(1n u==\'5T\'||u=="1"||u=="2")7d(Y(u,10)){5S 1:u=b.43;3P;5S 2:u=b.4q;3P;7g:b.c.m.1D(\'4Q u\')}H j(1n u==\'58\'){j(1n 47[u]==\'p\')u=47[u];H j(1n b[u]==\'p\')u=b[u];H b.c.m.1D(\'4Q u\')}8.d.u=u;j(1n 8.G!=\'3D\'){h l=8.G.M;18(h i=0;i<l;i++)8.G[i].J.59(u)}};b.c.X.w.6o=p(r){8.d.r=r;j(1n 8.G!=\'3D\'){h l=8.G.M;18(h i=0;i<l;i++){8.G[i].J.6o(r)}}};b.c.X.w.5a=p(v){8.d.3c=Y(1O/v,10);8.d.v=v;j(1n 8.G!=\'3D\'){h l=8.G.M;18(h i=0;i<l;i++){8.G[i].J.5a(v)}}};b.c.X.w.2J=p(4K){j(!8.k)q;j(3Z.M==0)4K=D;j(8.1u)8.4z();8.2W();h 4H=B 78();8.2D=4H.76();j(8.k.6Z)8.k=3f.70(8.k.6Z);j(8.2f!=0&&8.d.F){j(8.2f<1&&1n 8.d.u==\'p\'){h 4o=0;h 5h=8.d.r;h 2J=0;h 29=1;h 72=0;8.2f=1g.77(8.2f*1O)/1O;h 4p=D;h 3F=0;71(!4p){j(72++>8.d.r)3P;h 3S=4o+((5h-4o)/2);3F=1g.77(8.d.u(3S,1,-1,8.d.r)*1O)/1O;j(3F==8.2f){8.2D-=3S;4p=O}j(3F<8.2f){5h=3S;29=3F}H{4o=3S;2J=3F}}}8.2f=0}8.2t(\'3Q\',8);j(4K==D){h 1q=8;8.49=5x(p(){1q.4D()},8.d.3c)}8.1u=O};b.c.X.w.4A=p(){j(8.49){8e(8.49);8.49=z}8.2D=0};b.c.X.w.29=p(){8.4A();8.2t(\'3J\',8);8.1u=D};b.c.X.w.4z=p(){h 14=8.2I();j(8.2D>0&&14<8.d.r)8.2f=8.d.u(14,0,1,8.d.r);8.4A();8.2t(\'5G\',8);8.1u=D};b.c.X.w.4D=p(){h 1u=O;8.2t(\'4L\',8);h 75=8.2I();j(1n 8.d.u!=\'p\'){b.c.m.1D(\'4Q u\');q}8.3r();j(75>8.d.r){1u=D;8.29()}q 1u};b.c.X.w.2I=p(){j(8.2D>0){h 4H=B 78();q(4H.76()-8.2D)}q 0};b.c.X.w.K=p(){j(!8.E){8.E=b.15;q}j(8.d.F==O){j(8.E==b.15){8.E=b.1J;8.2t(\'5H\',8)}H j(8.E==b.1J){8.E=b.15}}};b.c.X.w.2W=p(){j(8.d&&8.d.F)8.K()};b.c.X.w.3r=p(){};b.c.X.w.4L=p(6X){j(6X!=8)8.2t(\'4L\',8)};b.c.1A=p(k,Z,13,d){8.4P=D;j(3Z.M==3){d=13;13=Z;Z=b.c.48(k);8.4P=O}b.c.X.1s(8,d);8.1t=\'1A\';8.k=b.c.1x(k);j(!8.k)q;j(Z.N!=13.N)b.c.m.1D(\'b.c.1A: 6H N (\'+Z.N+\', \'+13.N+\')\');8.N=Z.N;8.3H=4a(Z.x);8.46=4a(13.x);8.3s=4a(Z.y);8.42=4a(13.y)};b.c.1A.w=B b.c.X();b.c.1A.w.1v=b.c.1A;b.c.1A.w.3r=p(){h 1f=0;h 1b=0;h P=1g.P;h 14=8.2I();j(8.E==b.15){1f=P(8.d.u(14,8.3H,8.46-8.3H,8.d.r));1b=P(8.d.u(14,8.3s,8.42-8.3s,8.d.r))}H j(8.E==b.1J){1f=P(8.d.u(14,8.46,8.3H-8.46,8.d.r));1b=P(8.d.u(14,8.42,8.3s-8.42,8.d.r))}8.k.11.1f=1f+8.N;8.k.11.1b=1b+8.N};b.c.1A.w.2W=p(){j(8.d&&8.d.F)8.K();j(8.4P==O){h Z=b.c.48(8.k);8.3H=Z.x;8.3s=Z.y;8.8v=8.3H-8.46;8.8B=8.3s-8.42}};b.c.1P=p(k,T,12,d){8.5B=D;j(3Z.M==3){d=12;12=T;T=b.c.2n(k);8.5B=O}b.c.X.1s(8,d);8.1t=\'1P\';8.k=b.c.1x(k);j(!8.k)q;k=8.k;j(T.N!=12.N){b.c.m.1D(\'b.c.1P: 6H N (\'+T.N+\', \'+12.N+\')\');q D}8.N=T.N;h 1m=b.c.2n(k);8.2b=1m.A;8.2r=1m.C;8.1K=T.A;8.1M=T.C;8.28=12.A;8.2c=12.C;8.2g=B 63();j(8.d.1S){b.c.2L(8.k);h 1l=b.c.3W;8.6I=1l(k,\'4s-1b-A\');8.6J=1l(k,\'4s-4O-A\');8.6T=1l(k,\'4s-1f-A\');8.6S=1l(k,\'4s-4N-A\');8.6U=1l(k,\'4m-1b\');8.6V=1l(k,\'4m-4O\');8.6W=1l(k,\'4m-1f\');8.6R=1l(k,\'4m-4N\');8.6Q=1l(k,\'3n-1b\');8.6L=1l(k,\'3n-4O\');8.6K=1l(k,\'3n-4N\');8.6M=1l(k,\'3n-1f\');8.8i=1l(k,\'1f\');8.8b=1l(k,\'1b\')}j(8.d.2G)b.c.m.6A(k,8.2g);8.4M=1.0;h 2z=b.c.1a(8.k,\'80-5r\');j(2z&&/74\\s*$/.1j(2z))8.4M=85(2z);h 4c=b.c.m.1N;j(4c(8.1K)){h 6v=b.c.m.1k(8.1K);8.1K=1m.A*(6v/W)}j(4c(8.1M)){h 6u=b.c.m.1k(8.1M);8.1M=1m.C*(6u/W)}j(4c(8.28)){h 6w=b.c.m.1k(8.28);8.28=1m.A*(6w/W)}j(4c(8.2c)){h 6x=b.c.m.1k(8.2c);8.2c=1m.C*(6x/W)}8.2o=b.c.5i(8.k)};b.c.1P.w=B b.c.X();b.c.1P.w.1v=b.c.1P;b.c.1P.w.3r=p(){h A=0;h C=0;h 2z=0;h E=0;h P=1g.P;h 14=8.2I();j(8.E==b.15){A=P(8.d.u(14,8.1K,8.28-8.1K,8.d.r));C=P(8.d.u(14,8.1M,8.2c-8.1M,8.d.r));E=1}H j(8.E==b.1J){A=P(8.d.u(14,8.28,8.1K-8.28,8.d.r));C=P(8.d.u(14,8.2c,8.1M-8.2c,8.d.r));E=-1}h 2a=A/8.2b;2z=8.4M*2a;h 1w=8.k.11;j(A<0)A=0;j(C<0)C=0;1w.A=A+8.N;1w.C=C+8.N;j(1n 8.d.1S!=\'3D\'&&8.d.1S==O){h 1l=b.c.3W;h 79=1l(8.k,\'1b\');h 6C=1l(8.k,\'1f\');h 73=1l(8.k,\'3n-1b\');h 6D=1l(8.k,\'3n-1f\');h 39=2a;h 34=C/8.2r;h 6P=P(8.6I*34);h 6O=P(8.6J*34);h 6N=P(8.6T*39);h 6y=P(8.6S*39);h 6z=P(8.6U*34);h 6Y=P(8.6V*34);h 6B=P(8.6W*39);h 6G=P(8.6R*39);h 5t=P(8.6Q*34);h 6F=P(8.6L*34);h 6E=P(8.6K*39);h 5l=P(8.6M*39);1w.7X=6P+8.N;1w.7H=6O+8.N;1w.8C=6N+8.N;1w.7i=6y+8.N;1w.8s=6z+8.N;1w.8F=6Y+8.N;1w.8o=6B+8.N;1w.8f=6G+8.N;1w.8k=5t+8.N;1w.89=6F+8.N;1w.8D=5l+8.N;1w.8A=6E+8.N;1w.1f=P(6C+6D-5l)+8.N;1w.1b=P(79+73-5t)+8.N}j(8.d.2G){18(h i=0;i<8.2g.M;i++){8.2g[i][0].11.A=2a*8.2g[i][1]+8.N;8.2g[i][0].11.C=2a*8.2g[i][2]+8.N}8.k.11.2z=2z+\'74\'}j(8.2o){b.c.2o(8.k);8.2o=D}};b.c.1P.w.2W=p(){j(8.d&&8.d.F)8.K();j(8.5B==O){h T=b.c.4b(k);8.1K=T.A;8.1M=T.C;8.86=8.1K-8.28;8.8H=8.1M-8.2c}};b.c.24=p(k,2h,2v,d){8.5v=D;j(3Z.M==3){d=2v;2v=2h;2h=b.c.5w(k);8.5v=O}b.c.X.1s(8,d);8.1t=\'24\';8.k=b.c.1x(k);j(!8.k)q;j(/2M/.1j(27.26)&&(!8.k.8w))b.c.1h(8.k,\'83\',\'1\');8.2h=2h;8.2v=2v;8.2o=b.c.5i(8.k)};b.c.24.w=B b.c.X();b.c.24.w.1v=b.c.24;b.c.24.w.3r=p(){h 1E=0;h 14=8.2I();j(8.E==b.15)1E=8.d.u(14,8.2h,8.2v-8.2h,8.d.r);H j(8.E==b.1J)1E=8.d.u(14,8.2v,8.2h-8.2v,8.d.r);j(1E<0)1E=0;j(/2M/.1j(27.26)){h 4d=b.c.1a(8.k,\'4w\');j(4d){4d=4d.5E(/4r\\(1E=[0-9]{1,3}\\)/g,\'\')}8.k.11.4w=4d+"4r(1E="+1g.P(1E*W)+")"}H 8.k.11.1E=1E;j(8.2o){b.c.2o(8.k);8.2o=D}};b.c.24.w.2W=p(){j(8.d&&8.d.F)8.K();j(8.5v==O){8.2h=b.c.5w(k);8.8G=8.2h-8.2v}};b.c.2E=p(k,1U,2w,d){8.5u=D;j(3Z.M==3){d=2w;2w=1U;1U=b.c.4G(k);8.5u=O}b.c.X.1s(8,d);8.1t=\'2E\';8.k=b.c.1x(k);j(!8.k)q;8.1U=1U;8.2w=2w;8.3g=b.c.m.2e(1U.2u(1,2));8.3w=b.c.m.2e(1U.2u(3,2));8.3h=b.c.m.2e(1U.2u(5,2));8.3R=b.c.m.2e(2w.2u(1,2));8.3M=b.c.m.2e(2w.2u(3,2));8.3N=b.c.m.2e(2w.2u(5,2))};b.c.2E.w=B b.c.X();b.c.2E.w.1v=b.c.2E;b.c.2E.w.3r=p(){h 4h=0;h 4g=0;h 4f=0;h P=1g.P;h 14=8.2I();j(8.E==b.15){4h=P(8.d.u(14,8.3g,8.3R-8.3g,8.d.r));4g=P(8.d.u(14,8.3w,8.3M-8.3w,8.d.r));4f=P(8.d.u(14,8.3h,8.3N-8.3h,8.d.r))}H j(8.E==b.1J){4h=P(8.d.u(14,8.3R,8.3g-8.3R,8.d.r));4g=P(8.d.u(14,8.3M,8.3w-8.3M,8.d.r));4f=P(8.d.u(14,8.3N,8.3h-8.3N,8.d.r))}8.k.11.7F=b.c.m.3j(4h,4g,4f)};b.c.2E.w.2W=p(){j(8.d&&8.d.F)8.K();j(8.5u==O){8.1U=b.c.4G(k);8.3g=b.c.m.2e(1U.2u(1,2));8.3w=b.c.m.2e(1U.2u(3,2));8.3h=b.c.m.2e(1U.2u(5,2));8.7b=8.3g-8.3R;8.7v=8.3w-8.3M;8.7u=8.3h-8.3N}};b.c.U=p(d){b.c.X.1s(8,d);8.1t=\'U\';8.G=B 63();8.1C=-1;h 62=p(J,45){8.J=J;8.45=45;8.1u=D};8.5s=62};b.c.U.w=B b.c.X();b.c.U.w.1v=b.c.U;b.c.U.w.5x=p(3c){h l=8.G.M;8.d.3c=3c;18(h i=0;i<l;i++){8.G[i].J.5x(3c)}};b.c.U.w.4D=p(){h 1u=O;h 3q=D;h 5y=D;h 5A=D;j((8.1C==-1&&8.E==b.15)||(8.1C==8.G.M&&8.E==b.1J))8.5z();h 2J=8.E==b.15?0:8.G.M-1;h 29=8.E==b.15?8.G.M:-1;h 3O=8.E==b.15?1:-1;18(h i=2J;i!=29;i+=3O){j(8.G[i].1u==O){5y=8.G[i].J.4D();j(5y==D&&i==8.1C){8.G[i].1u=D;5A=O}}}j(5A==O)3q=8.5z();j(3q==O){8.29();1u=D;18(h i=0;i<8.G.M;i++)8.G[i].1u=D;8.1C=8.E==b.15?8.G.M:-1}q 1u};b.c.U.w.5z=p(){h 3q=D;h 3O=8.E==b.15?1:-1;h 29=8.E==b.15?8.G.M:-1;8.1C+=3O;j((8.1C>(8.G.M-1)&&8.E==b.15)||(8.1C<0&&8.E==b.1J))3q=O;H 18(h i=8.1C;i!=29;i+=3O){j((i>8.1C&&8.E==b.15||i<8.1C&&8.E==b.1J)&&8.G[i].45=="5K")3P;8.G[i].J.2J(O);8.G[i].1u=O;8.1C=i}q 3q};b.c.U.w.5m=p(){j(!8.E){8.E=b.15;q}j(8.d.F==O){j(8.E==b.15){8.E=b.1J;8.2t(\'5H\',8);8.1C=8.G.M}H j(8.E==b.1J){8.E=b.15;8.1C=-1}}H{j(8.E==b.15)8.1C=-1;H j(8.E==b.1J)8.1C=8.G.M}};b.c.U.w.K=p(){8.5m();18(h i=0;i<8.G.M;i++){j(8.G[i].J.d&&(8.G[i].J.d.F!=z))j(8.G[i].J.d.F==O)8.G[i].J.K()}};b.c.U.w.4z=p(){18(h i=0;i<8.G.M;i++)j(8.G[i].J.1u)8.G[i].J.4z();h 14=8.2I();j(8.2D>0&&14<8.d.r)8.2f=8.d.u(14,0,1,8.d.r);8.4A();8.2t(\'5G\',8);8.1u=D};b.c.U.w.1V=p(J){J.2N(8);8.G[8.G.M]=B 8.5s(J,"5K");j(8.G.M==1){8.k=J.k}};b.c.U.w.2k=p(J){j(8.G.M==0||8.G[8.G.M-1].45!=\'5P\')J.2N(8);8.G[8.G.M]=B 8.5s(J,"5P");j(8.G.M==1){8.k=J.k}};b.c.U.w.2W=p(){8.5m()};b.c.30=p(k,d){j(!8.1Y)q b.c.m.21(\'30\');b.c.U.1s(8,d);8.1t=\'30\';h k=b.c.1x(k);8.k=k;j(!8.k)q;h R=1O;h 1r=0.0;h 1p=W.0;h K=D;h u=b.3I;h v=60;h 1I=0;j(/2M/.1j(27.26))1I=Y(b.c.2Z(8.k,\'4w\').5E(/4r\\(1E=([0-9]{1,3})\\)/g,\'$1\'),10);H 1I=Y(b.c.2Z(8.k,\'1E\')*W,10);j(5F(1I))1I=W;j(d){j(d.r!=z)R=d.r;j(d.L!=z){j(b.c.m.1N(d.L))1r=b.c.m.1k(d.L)*1I/W;H 1r=d.L}j(d.I!=z){j(b.c.m.1N(d.I))1p=b.c.m.1k(d.I)*1I/W;H 1p=d.I}j(d.F!=z)K=d.F;j(d.u!=z)u=d.u;j(d.v!=z)v=d.v;H 8.d.u=u}1r=1r/W.0;1p=1p/W.0;d={r:R,F:K,u:u,L:1r,I:1p,v:v};h 4x=B b.c.24(k,1r,1p,d);8.1V(4x)};b.c.30.w=B b.c.U();b.c.30.w.1v=b.c.30;b.c.2Y=p(k,d){j(!8.1Y)q b.c.m.21(\'2Y\');b.c.U.1s(8,d);8.1t=\'2Y\';h k=b.c.1x(k);8.k=k;j(!8.k)q;h R=1O;h K=D;h V=b.5Z;h v=60;h 2F=D;b.c.3l(k);h 1m=b.c.2n(k);h 4l=1m.C;h 4I=0;h 4u=d?d.L:1m.C;h 4t=d?d.I:0;h 2j=D;j(d){j(d.r!=z)R=d.r;j(d.L!=z){j(b.c.m.1N(d.L))4l=b.c.m.1k(d.L)*1m.C/W;H 4l=b.c.m.2A(d.L)}j(d.I!=z){j(b.c.m.1N(d.I))4I=b.c.m.1k(d.I)*1m.C/W;H 4I=b.c.m.2A(d.I)}j(d.F!=z)K=d.F;j(d.u!=z)V=d.u;j(d.v!=z)v=d.v;j(d.1S!=z)2j=d.1S}h T=B b.c.m.2d;T.A=1m.A;T.C=4l;h 12=B b.c.m.2d;12.A=1m.A;12.C=4I;d={r:R,F:K,u:V,2G:2F,1S:2j,L:4u,I:4t,v:v};h 6s=B b.c.1P(k,T,12,d);8.1V(6s)};b.c.2Y.w=B b.c.U();b.c.2Y.w.1v=b.c.2Y;b.c.2O=p(k,d){j(!8.1Y)q b.c.m.21(\'2O\');b.c.U.1s(8,d);8.1t=\'2O\';h R=1O;h 1B="#8u";h K=D;h V=b.4q;h v=60;h k=b.c.1x(k);8.k=k;j(!8.k)q;h 1o=b.c.4G(k);j(1o=="8q")1o="#8r";j(d){j(d.r!=z)R=d.r;j(d.L!=z)1o=d.L;j(d.I!=z)1B=d.I;j(d.F!=z)K=d.F;j(d.u!=z)V=d.u;j(d.v!=z)v=d.v}j(1o.25(\'3j\')!=-1)h 1o=b.c.m.3j(Y(1o.2s(1o.25(\'(\')+1,1o.25(\',\')),10),Y(1o.2s(1o.25(\',\')+1,1o.33(\',\')),10),Y(1o.2s(1o.33(\',\')+1,1o.25(\')\')),10));j(1B.25(\'3j\')!=-1)h 1B=b.c.m.3j(Y(1B.2s(1B.25(\'(\')+1,1B.25(\',\')),10),Y(1B.2s(1B.25(\',\')+1,1B.33(\',\')),10),Y(1B.2s(1B.33(\',\')+1,1B.25(\')\')),10));h 1o=b.c.m.5k(1o);h 1B=b.c.m.5k(1B);8.6m=b.c.1a(k,\'4e-5j\');d={r:R,F:K,u:V,v:v};h 6k=B b.c.2E(k,1o,1B,d);8.1V(6k);8.2N({3Q:p(J){b.c.1h(J.k,\'4e-5j\',\'1Z\')},3J:p(J){b.c.1h(J.k,\'4e-5j\',J.6m);j(J.E==b.15&&J.d.6n)b.c.1h(k,\'4e-2K\',J.d.6n)}})};b.c.2O.w=B b.c.U();b.c.2O.w.1v=b.c.2O;b.c.2P=p(k,d){j(!8.1Y)q b.c.m.21(\'2P\');b.c.U.1s(8,d);8.1t=\'2P\';h k=b.c.1x(k);8.k=k;j(!8.k)q;h R=1O;h K=D;h V=b.4q;h v=60;h 31=D;h 2i=b.c.m.6r(k);h E=-1;j(/2M 7.0/.1j(27.26)&&/5n 5D/.1j(27.26))b.c.2L(k);b.c.3l(k);j(/2M 6.0/.1j(27.26)&&/5n 5D/.1j(27.26)){h 1F=b.c.1a(k,\'1L\');j(1F&&(1F==\'5o\'||1F==\'8l\')){b.c.1h(k,\'1L\',\'6g\');b.c.1h(k,\'1b\',\'\');b.c.1h(k,\'1f\',\'\')}}j(2i){b.c.2L(2i);b.c.3l(2i);h 68=b.c.2n(2i,k);b.c.1h(2i,\'A\',68.A+\'1Q\')}h 22=b.c.2n(k);h 2S=B b.c.m.2d();h 1X=B b.c.m.2d();2S.A=1X.A=22.A;2S.C=1X.C=22.C;j(!8.d.I){j(!d)d={};d.I=\'0%\'}j(d&&d.67!==z&&d.67===O)31=O;j(d.r!=z)R=d.r;j(d.L!=z){j(31){j(b.c.m.1N(d.L))22.A=2S.A*b.c.m.1k(d.L)/W;H 22.A=b.c.m.2A(d.L)}H{j(b.c.m.1N(d.L))22.C=2S.C*b.c.m.1k(d.L)/W;H 22.C=b.c.m.2A(d.L)}}j(d.I!=z){j(31){j(b.c.m.1N(d.I))1X.A=2S.A*b.c.m.1k(d.I)/W;H 1X.A=b.c.m.2A(d.I)}H{j(b.c.m.1N(d.I))1X.C=2S.C*b.c.m.1k(d.I)/W;H 1X.C=b.c.m.2A(d.I)}}j(d.F!=z)K=d.F;j(d.u!=z)V=d.u;j(d.v!=z)v=d.v;d={r:R,u:V,2G:D,F:K,v:v};h 5r=B b.c.1P(k,22,1X,d);8.2k(5r);j((22.A<1X.A&&31)||(22.C<1X.C&&!31))E=1;h Z=B b.c.m.1G();h 13=B b.c.m.1G();13.x=Z.x=b.c.3W(2i,\'1f\');13.y=Z.y=b.c.3W(2i,\'1b\');13.N=Z.N;j(31)13.x=Y(Z.x+E*(22.A-1X.A),10);H 13.y=Y(Z.y+E*(22.C-1X.C),10);j(E==1){h 3x=Z;h Z=13;h 13=3x}d={r:R,u:V,F:K,L:Z,I:13,v:v};h 66=B b.c.1A(2i,Z,13,d);8.2k(66)};b.c.2P.w=B b.c.U();b.c.2P.w.1v=b.c.2P;b.c.2q=p(k,d){j(!k)q;j(!8.1Y)q b.c.m.21(\'2q\');b.c.U.1s(8,d);8.1t=\'2q\';h R=1O;h K=D;h 2F=O;h 5q=D;h 5p=O;h 2j=D;h V=b.6a;h v=60;h k=b.c.1x(k);8.k=k;j(!8.k)q;b.c.3l(k);h 4v=b.c.2n(k);h 2b=4v.A;h 2r=4v.C;h 2a=(2b==0)?1:2r/2b;h T=B b.c.m.2d;T.A=0;T.C=0;h 12=B b.c.m.2d;12.A=2b;12.C=2r;h 4u=d?d.L:4v.A;h 4t=d?d.I:0;h 2p=b.c.m.2A;j(d){j(d.5C!=z)5p=d.5C;j(d.r!=z)R=d.r;j(d.1S!=z)2j=d.1S;j(d.2G!=z)2F=d.2G;j(d.L!=z){j(b.c.m.1N(d.L)){T.A=2b*(b.c.m.1k(d.L)/W);T.C=2r*(b.c.m.1k(d.L)/W)}H{j(5q){T.C=2p(d.L);T.A=2p(d.L)/2a}H{T.A=2p(d.L);T.C=2a*2p(d.L)}}}j(d.I!=z){j(b.c.m.1N(d.I)){12.A=2b*(b.c.m.1k(d.I)/W);12.C=2r*(b.c.m.1k(d.I)/W)}H{j(5q){12.C=2p(d.I);12.A=2p(d.I)/2a}H{12.A=2p(d.I);12.C=2a*2p(d.I)}}}j(d.F!=z)K=d.F;j(d.u!=z)V=d.u;j(d.v!=z)v=d.v}d={r:R,F:K,u:V,2G:2F,1S:2j,v:v};h 3G=B b.c.1P(k,T,12,d);8.2k(3G);j(5p){b.c.2L(k);h Q=B b.c.m.1G();Q.x=Y(b.c.2Z(k,"1f"),10);Q.y=Y(b.c.2Z(k,"1b"),10);j(!Q.x)Q.x=0;j(!Q.y)Q.y=0;d={r:R,F:K,u:V,L:4u,I:4t,v:v};h Z=B b.c.m.1G;Z.x=Q.x+(2b-T.A)/2.0;Z.y=Q.y+(2r-T.C)/2.0;h 13=B b.c.m.1G;13.x=Q.x+(2b-12.A)/2.0;13.y=Q.y+(2r-12.C)/2.0;h 3v=B b.c.1A(k,Z,13,d);8.2k(3v)}};b.c.2q.w=B b.c.U();b.c.2q.w.1v=b.c.2q;b.c.2H=p(k,d){j(!8.1Y)q b.c.m.21(\'2H\');b.c.U.1s(8,d);8.d.E=D;j(8.d.F)8.d.F=D;8.1t=\'2H\';h k=b.c.1x(k);8.k=k;j(!8.k)q;h R=W;h V=b.43;h v=60;h 3u=4;j(d){j(d.r!=z)3u=1g.4n(8.d.r/R)-1;j(d.v!=z)v=d.v;j(d.u!=z)V=d.u}b.c.2L(k);h Q=B b.c.m.1G();Q.x=Y(b.c.1a(k,"1f"),10);Q.y=Y(b.c.1a(k,"1b"),10);j(!Q.x)Q.x=0;j(!Q.y)Q.y=0;h 41=B b.c.m.1G;41.x=Q.x;41.y=Q.y;h 3b=B b.c.m.1G;3b.x=Q.x+20;3b.y=Q.y+0;h 3t=B b.c.m.1G;3t.x=Q.x+ -20;3t.y=Q.y+0;d={r:1g.4n(R/2),F:D,v:v,u:V};h J=B b.c.1A(k,41,3b,d);8.1V(J);d={r:R,F:D,v:v,u:V};h 6d=B b.c.1A(k,3b,3t,d);h 6c=B b.c.1A(k,3t,3b,d);18(h i=0;i<3u;i++){j(i%2==0)8.1V(6d);H 8.1V(6c)}h 1F=(3u%2==0)?3b:3t;d={r:1g.4n(R/2),F:D,v:v,u:V};h J=B b.c.1A(k,1F,41,d);8.1V(J)};b.c.2H.w=B b.c.U();b.c.2H.w.1v=b.c.2H;b.c.2H.w.K=p(){};b.c.2U=p(k,d){j(!8.1Y)q b.c.m.21(\'2U\');j(!d)d={};j(!d.I)d.I=\'0%\';j(!d.L)d.L=\'W%\';d.5C=D;b.c.2q.1s(8,k,d);8.1t=\'2U\'};b.c.2U.w=B b.c.2q();b.c.2U.w.1v=b.c.2U;b.c.2B=p(k,d){j(!8.1Y)q b.c.m.21(\'2B\');b.c.U.1s(8,d);8.d.E=D;j(8.d.F)8.d.F=D;h k=b.c.1x(k);h 1I=0;8.k=k;j(!8.k)q;8.1t=\'2B\';h R=W;h 1r=W.0;h 1p=0.0;h K=D;h V=b.43;h v=60;j(/2M/.1j(27.26))1I=Y(b.c.2Z(8.k,\'4w\').5E(/4r\\(1E=([0-9]{1,3})\\)/g,\'$1\'),10);H 1I=Y(b.c.2Z(8.k,\'1E\')*W,10);j(5F(1I)){1I=W}j(d){j(d.L!=z){j(b.c.m.1N(d.L))1r=b.c.m.1k(d.L)*1I/W;H 1r=d.L}j(d.I!=z){j(b.c.m.1N(d.I))1p=b.c.m.1k(d.I)*1I/W;H 1p=d.I}j(d.u!=z)V=d.u;j(d.v!=z)v=d.v}d={r:R,F:K,u:V,v:v};1r=1r/W.0;1p=1p/W.0;h 4x=B b.c.24(k,1r,1p,d);h 6p=B b.c.24(k,1p,1r,d);h 3u=Y(8.d.r/7o,10);18(h i=0;i<3u;i++){8.1V(4x);8.1V(6p)}};b.c.2B.w=B b.c.U();b.c.2B.w.1v=b.c.2B;b.c.2B.w.K=p(){};b.c.3a=p(k,d){j(!8.1Y)q b.c.m.21(\'3a\');b.c.U.1s(8,d);h k=b.c.1x(k);8.k=k;j(!8.k)q;8.1t=\'3a\';h K=D;h 2F=D;h R=1O;h V=b.3I;h v=60;b.c.2L(k);j(d){j(d.F!=z)K=d.F;j(d.r!=z)R=d.r;j(d.u!=z)V=d.u;j(d.v!=z)v=d.v}h 1m=b.c.4b(k);h 1K=1m.A;h 1M=1m.C;d={r:R,F:K,u:V,v:v};h 1r=1.0;h 1p=0.0;h 4B=B b.c.24(k,1r,1p,d);8.2k(4B);h Z=b.c.48(k);h 13=B b.c.m.1G;13.x=1K/2.0*-1.0;13.y=1M/2.0*-1.0;d={r:R,F:K,u:V,L:Z,I:13,v:v};h 3v=B b.c.1A(k,Z,13,d);8.2k(3v);h 1q=8;8.2N({3Q:p(){j(1q.E==b.1J){1q.k.11.1R=\'3T\'}},3J:p(){j(1q.E==b.15){1q.k.11.1R=\'1Z\'}}})};b.c.3a.w=B b.c.U;b.c.3a.w.1v=b.c.3a;b.c.2T=p(k,d){j(!8.1Y)q b.c.m.21(\'2T\');b.c.U.1s(8,d);h k=b.c.1x(k);8.k=k;j(!8.k)q;h R=1O;h v=60;h V=b.3I;h E=b.15;h K=D;8.1t=\'2T\';b.c.2L(k);j(d){j(d.r!=z)R=d.r;j(d.F!=z)K=d.F;j(d.v!=z)v=d.v;j(d.u!=z)V=d.u;j(d.7p!=z)E=-1}h Q=B b.c.m.1G();Q.x=Y(b.c.1a(k,"1f"),10);Q.y=Y(b.c.1a(k,"1b"),10);j(!Q.x)Q.x=0;j(!Q.y)Q.y=0;h Z=B b.c.m.1G;Z.x=Q.x+0;Z.y=Q.y+0;h 13=B b.c.m.1G;13.x=Q.x+0;13.y=Q.y+(E*7t);d={L:Z,I:13,r:R,F:K,u:V,v:v};h 3v=B b.c.1A(k,d.L,d.I,d);8.2k(3v);h 1r=1.0;h 1p=0.0;d={r:R,F:K,u:V,v:v};h 4B=B b.c.24(k,1r,1p,d);8.2k(4B);h 1q=8;8.2N({3Q:p(){1q.k.11.1R=\'3T\'},3J:p(){j(1q.E==b.15){1q.k.11.1R=\'1Z\'}}})};b.c.2T.w=B b.c.U();b.c.2T.w.1v=b.c.2T;b.c.2V=p(k,d){j(!8.1Y)q b.c.m.21(\'2V\');b.c.U.1s(8,d);h k=b.c.1x(k);8.k=k;j(!8.k)q;8.1t=\'2V\';h R=1O;h K=D;h 2F=O;h 2j=D;h V=b.3I;h v=v;b.c.3l(k);h 1m=b.c.2n(k);h 1K=1m.A;h 1M=1m.C;h 28=1K;h 2c=1M/5;h T=B b.c.m.2d;T.A=1K;T.C=1M;h 12=B b.c.m.2d;12.A=28;12.C=2c;j(d){j(d.r!=z)R=1g.4n(d.r/2);j(d.F!=z)K=d.F;j(d.1S!=z)2j=d.1S;j(d.v!=z)v=d.v;j(d.u!=z)V=d.u}d={r:R,F:K,2G:2F,1S:2j,u:V,v:v};h 3G=B b.c.1P(k,T,12,d);8.1V(3G);T.A=12.A;T.C=12.C;12.A=\'0%\';h 3G=B b.c.1P(k,T,12,d);8.1V(3G)};b.c.2V.w=B b.c.U();b.c.2V.w.1v=b.c.2V;b.c.7T=p(k,d){q b.c.m.23(\'30\',k,d)};b.c.7U=p(k,d){q b.c.m.23(\'2Y\',k,d)};b.c.7L=p(k,d){q b.c.m.23(\'2O\',k,d)};b.c.7K=p(k,d){q b.c.m.23(\'2P\',k,d)};b.c.7B=p(k,d){q b.c.m.23(\'2q\',k,d)};b.c.7C=p(k,d){q b.c.m.23(\'2H\',k,d)};b.c.7A=p(k,d){q b.c.m.23(\'2U\',k,d)};b.c.7z=p(k,d){q b.c.m.23(\'2B\',k,d)};b.c.7x=p(k,d){q b.c.m.23(\'3a\',k,d)};b.c.7y=p(k,d){q b.c.m.23(\'2T\',k,d)};b.c.7I=p(k,d){q b.c.m.23(\'2V\',k,d)};',62,541,'||||||||this|||Spry|Effect|options||||var||if|element||Utils|||function|return|duration|||transition|fps|prototype|||null|width|new|height|false|direction|toggle|effectsArray|else|to|effect|doToggle|from|length|units|true|floor|startOffsetPosition|durationInMilliseconds||fromRect|Cluster|kindOfTransition|100|Animator|parseInt|fromPos||style|toRect|toPos|elapsed|forwards|||for|time|getStyleProp|top|begin|computedStyle|change|left|Math|setStyleProp|value|test|getPercentValue|intProp|originalRect|typeof|fromColor|toOpacity|self|fromOpacity|call|name|isRunning|constructor|elStyle|getElement|prop|refElement|Move|toColor|currIdx|showError|opacity|pos|Position|result|originalOpacity|backwards|startWidth|position|startHeight|isPercentValue|1000|Size|px|display|useCSSBox|dimensions|startColor|addNextEffect|tryComputedStyle|toDim|notStaticAnimator|none||showInitError|fromDim|DoEffect|Opacity|indexOf|userAgent|navigator|stopWidth|stop|propFactor|originalWidth|stopHeight|Rectangle|hexToInt|cancelRemaining|childImages|startOpacity|firstChildElt|fullCSSBox|addParallelEffect|visibility|observers|getDimensionsRegardlessOfDisplayState|enforceVisible|pixelValue|Grow|originalHeight|substring|notifyObservers|substr|stopOpacity|stopColor|getComputedStyle|Notifier|fontSize|getPixelValue|Pulsate|len|startMilliseconds|Color|doScaleContent|scaleContent|Shake|getElapsedMilliseconds|start|color|makePositioned|MSIE|addObserver|Highlight|Slide|ele|effects|initDim|DropOut|Squish|Fold|prepareStart|node|Blind|getStylePropRegardlessOfDisplayState|Fade|slideHorizontally|effectB|lastIndexOf|heightFactor|optionsA|observer|catch|try|widthFactor|Puff|rightPos|interval|hidden|childCurr|document|startRedColor|startBlueColor|displayElement|rgb|displayOrig|makeClipping|intToHex|margin|finish|optionsB|allEffectsDidRun|animate|startY|leftPos|steps|moveEffect|startGreenColor|tmp|obs|overflow|setup|propVisible|toLowerCase|undefined|propDisplay|middle|sizeEffect|startX|fifthTransition|onPostEffect|in|startEltIn|stopGreenColor|stopBlueColor|step|break|onPreEffect|stopRedColor|half|block|effectA|Registry|intPropStyle|methodName|suppressNotifications|arguments|ef|centerPos|stopY|linearTransition|oStringList|kind|stopX|window|getPosition|timer|Number|getDimensions|isPercent|tmpval|background|blueColor|greenColor|redColor|compositeColorHex|unitIndex|stringToCamelize|fromHeightPx|padding|ceil|startTime|found|sinusoidalTransition|alpha|border|optionTo|optionFrom|dimRect|filter|fadeEffect|camelizedString|cancel|stopFlagReset|opacityEffect|pow|drawEffect|visibilityOrig|eff|getBgColor|currDate|toHeightPx|defaultView|withoutTimer|onStep|fontFactor|right|bottom|dynamicFromPos|unknown|SpryRegistry|objectCountA|dimensionsCurr|objectCountB|typeA|getRegisteredEffect|imgCurr|targetImagesOut|Transitions|sqrt|trans|compare|isFirstEntry|effectName|data|opera|currentStyle|string|setTransition|setFps|nodeType|heightCache|css|camelized|camelize|needsCache|stopTime|isInvisible|image|longColorVersion|margin_left|toggleCluster|Windows|static|growFromCenter|calcHeight|size|ClusteredEffect|margin_top|dynamicStartColor|dynamicStartOpacity|getOpacity|setInterval|baseEffectIsStillRunning|initNextEffectsRunning|evalNextEffectsRunning|dynamicFromRect|growCenter|NT|replace|isNaN|onCancel|onToggle|blueInt|greenInt|queue|redHex|focus|blueHex|greenHex|parallel|redInt|integerNum|case|number|effectsAreTheSame|PI|cos|setOptions|callback|circleTransition||msg|_ClusteredEffect|Array|hexStr|cleanWhitespace|move|horizontal|childRect|enableNotifications|squareTransition|optionsAreIdentical|effectToLeft|effectToRight|addEffect|typeB|relative|imageCnt|toUpperCase|childCountInit|highlightEffect|split|restoreBackgroundImage|restoreColor|setDuration|appearEffect|styleProp|getFirstChildElement|blindEffect|childNodes|startHeightPercent|startWidthPercent|stopWidthPercent|stopHeightPercent|border_right|padding_top|fetchChildImages|padding_left|origLeft|origMarginLeft|margin_right|margin_bottom|padding_right|Conflicting|startFromBorder_top|startFromBorder_bottom|startFromMargin_right|startFromMargin_bottom|startFromMargin_left|border_left|border_bottom|border_top|startFromMargin_top|startFromPadding_right|startFromBorder_right|startFromBorder_left|startFromPadding_top|startFromPadding_bottom|startFromPadding_left|el|padding_bottom|id|getElementById|while|emergency|origMarginTop|em|timeElapsed|getTime|round|Date|origTop|concat|redColorRange|Effects|switch|documentation|migration|default|toString|borderRightWidth|match|nextSibling|firstChild|hasChildNodes|img|200|dropIn|charAt|read|9a|160|blueColorRange|greenColorRange|accessed|DoPuff|DoDropOut|DoPulsate|DoSquish|DoGrow|DoShake|growSpecificTransition|sin|backgroundColor|squarerootTransition|borderBottomWidth|DoFold|pulsateTransition|DoSlide|DoHighlight|be|can|push|as|anymore|class|The|DoFade|DoBlind|alert|ERR|borderTopWidth|Please|getElementsByTagName|font|scroll|Unbalanced|zoom|disableNotifications|parseFloat|widthRange|getOffsetPosition|splice|marginBottom|offsetTop|startTop|Element|auto|clearInterval|paddingRight|offsetLeft|visible|startLeft|not|marginTop|fixed|getPropertyValue|removeObserver|paddingLeft|offsetWidth|transparent|ffff99|paddingTop|offsetHeight|ffffff|rangeMoveX|hasLayout|propA|propB|removeChild|marginRight|rangeMoveY|borderLeftWidth|marginLeft|repeat|paddingBottom|opacityRange|heightRange|nodeValue'.split('|'),0,{}))
--- a/includes/clientside/static/ajax.js	Sun Sep 30 19:22:04 2007 -0400
+++ b/includes/clientside/static/ajax.js	Sun Oct 07 21:31:14 2007 -0400
@@ -822,6 +822,33 @@
   window.location = loc;
 }
 
+var navto_ns;
+var navto_pg;
+var navto_ul;
+
+function ajaxLoginNavTo(namespace, page_id, min_level)
+{
+  // IE <6 pseudo-compatibility
+  if ( KILL_SWITCH )
+    return true;
+  navto_pg = page_id;
+  navto_ns = namespace;
+  navto_ul = min_level;
+  if ( auth_level < min_level )
+  {
+    ajaxPromptAdminAuth(function(k) {
+      ENANO_SID = k;
+      auth_level = navto_ul;
+      var loc = makeUrlNS(navto_ns, navto_pg);
+      if ( (ENANO_SID + ' ').length > 1 )
+        window.location = loc;
+    }, min_level);
+    return false;
+  }
+  var loc = makeUrlNS(navto_ns, navto_pg);
+  window.location = loc;
+}
+
 function ajaxAdminUser(username)
 {
   // IE <6 pseudo-compatibility
@@ -1142,7 +1169,6 @@
       keepalive_interval = setInterval('ajaxPingServer();', 600000);
     var span = document.getElementById('keepalivestat');
     span.firstChild.nodeValue = 'Turn off keep-alive';
-    ajaxPingServer();
   }
   else
   {
@@ -1155,6 +1181,36 @@
 
 function aboutKeepAlive()
 {
-  new messagebox(MB_OK|MB_ICONINFORMATION, 'About the keep-alive feature', 'Keep-alive is a new Enano feature that keeps your administrative session from timing out while you are using the administration panel. This feature can be useful if you are editing a large page or doing something in the administration interface that will take longer than 15 minutes.<br /><br />For security reasons, Enano mandates that high-privilege logins last only 15 minutes, with the time being reset each time a page is loaded (or, more specifically, each time the session API is started). The consequence of this is that if you are performing an action in the administration panel that takes more than 15 minutes, your session may be terminated. The keep-alive feature attempts to relieve this by sending a "ping" to the server every 10 minutes.<br /><br />Please note that keep-alive state is determined by a cookie. Thus, if you log out and then back in as a different administrator, keep-alive will use the same setting that was used when you were logged in as the first administrative user. In the same way, if you log into the administration panel under your account from another computer, keep-alive will be set to "off".');
+  new messagebox(MB_OK|MB_ICONINFORMATION, 'About the keep-alive feature', 'Keep-alive is a new Enano feature that keeps your administrative session from timing out while you are using the administration panel. This feature can be useful if you are editing a large page or doing something in the administration interface that will take longer than 15 minutes.<br /><br />For security reasons, Enano mandates that high-privilege logins last only 15 minutes, with the time being reset each time a page is loaded (or, more specifically, each time the session API is started). The consequence of this is that if you are performing an action in the administration panel that takes more than 15 minutes, your session may be terminated. The keep-alive feature attempts to relieve this by sending a "ping" to the server every 10 minutes.<br /><br />Please note that keep-alive state is determined by a cookie. Thus, if you log out and then back in as a different administrator, keep-alive will use the same setting that was used when you were logged in as the first administrative user. In the same way, if you log into the administration panel under your account from another computer, keep-alive will be set to "off".<br /><br /><b>For more information:</b><br /><a href="http://docs.enanocms.org/Help:Appendix_B" onclick="window.open(this.href); return false;">Overview of Enano'+"'"+'s security model');
 }
 
+function ajaxShowCaptcha(code)
+{
+  var mydiv = document.createElement('div');
+  mydiv.style.backgroundColor = '#FFFFFF';
+  mydiv.style.padding = '10px';
+  mydiv.style.position = 'absolute';
+  mydiv.style.top = '0px';
+  mydiv.id = 'autoCaptcha';
+  var img = document.createElement('img');
+  img.onload = function()
+  {
+    if ( this.loaded )
+      return true;
+    var mydiv = document.getElementById('autoCaptcha');
+    var width = getWidth();
+    var divw = $(mydiv).Width();
+    var left = ( width / 2 ) - ( divw / 2 );
+    mydiv.style.left = left + 'px';
+    fly_in_top(mydiv, false, true);
+    this.loaded = true;
+  };
+  img.src = makeUrlNS('Special', 'Captcha/' + code);
+  img.onclick = function() { this.src = this.src + '/a'; };
+  img.style.cursor = 'pointer';
+  mydiv.appendChild(img);
+  domObjChangeOpac(0, mydiv);
+  var body = document.getElementsByTagName('body')[0];
+  body.appendChild(mydiv);
+}
+
--- a/includes/clientside/static/enano-lib-basic.js	Sun Sep 30 19:22:04 2007 -0400
+++ b/includes/clientside/static/enano-lib-basic.js	Sun Oct 07 21:31:14 2007 -0400
@@ -283,6 +283,7 @@
   'flyin.js',
   'paginate.js',
   'pwstrength.js',
+  'SpryEffects.js',
   'loader.js'
 ];
 
--- a/includes/clientside/static/misc.js	Sun Sep 30 19:22:04 2007 -0400
+++ b/includes/clientside/static/misc.js	Sun Oct 07 21:31:14 2007 -0400
@@ -301,6 +301,53 @@
 var ajax_auth_prompt_cache = false;
 var ajax_auth_mb_cache = false;
 var ajax_auth_level_cache = false;
+var ajax_auth_error_string = false;
+var ajax_auth_show_captcha = false;
+
+function ajaxAuthErrorToString($data)
+{
+  var $errstring = $data.error;
+  // this was literally copied straight from the PHP code.
+  switch($data.error)
+  {
+    case 'key_not_found':
+      $errstring = 'Enano couldn\'t look up the encryption key used to encrypt your password. This most often happens if a cache rotation occurred during your login attempt, or if you refreshed the login page.';
+      break;
+    case 'key_wrong_length':
+      $errstring = 'The encryption key was the wrong length.';
+      break;
+    case 'too_big_for_britches':
+      $errstring = 'You are trying to authenticate at a level that your user account does not permit.';
+      break;
+    case 'invalid_credentials':
+      $errstring = 'You have entered an invalid username or password. Please enter your login details again.';
+      if ( $data.lockout_policy == 'lockout' )
+      {
+        $errstring += ' You have used up '+$data['lockout_fails']+' out of '+$data['lockout_threshold']+' login attempts. After you have used up all '+$data['lockout_threshold']+' login attempts, you will be locked out from logging in for '+$data['lockout_duration']+' minutes.';
+      }
+      else if ( $data.lockout_policy == 'captcha' )
+      {
+        $errstring += ' You have used up '+$data['lockout_fails']+' out of '+$data['lockout_threshold']+' login attempts. After you have used up all '+$data['lockout_threshold']+' login attempts, you will have to enter a visual confirmation code before logging in, effective for '+$data['lockout_duration']+' minutes.';
+      }
+      break;
+    case 'backend_fail':
+      $errstring = 'You entered the right credentials and everything was validated, but for some reason Enano couldn\'t register your session. This is an internal problem with the site and you are encouraged to contact site administration.';
+      break;
+    case 'locked_out':
+      $attempts = parseInt($data['lockout_fails']);
+      if ( $attempts > $data['lockout_threshold'])
+        $attempts = $data['lockout_threshold'];
+      window.console.debug('server time ', $data.server_time, ', last time ', $data['lockout_last_time'], ', duration ', $data['lockout_duration']);
+      $time_rem = $data.lockout_duration - Math.round( ( $data.server_time - $data.lockout_last_time ) / 60 );
+      $s = ( $time_rem == 1 ) ? '' : 's';
+      $errstring = "You have used up all "+$data['lockout_threshold']+" allowed login attempts. Please wait "+$time_rem+" minute"+$s+" before attempting to log in again";
+      if ( $data['lockout_policy'] == 'captcha' )
+        $errstring += ', or enter the visual confirmation code shown above in the appropriate box';
+      $errstring += '.';
+      break;
+  }
+  return $errstring;
+}
 
 function ajaxPromptAdminAuth(call_on_ok, level)
 {
@@ -319,6 +366,17 @@
   var title = ( level > USER_LEVEL_MEMBER ) ? 'You are requesting a sensitive operation.' : 'Please enter your username and password to continue.';
   ajax_auth_mb_cache = new messagebox(MB_OKCANCEL|MB_ICONLOCK, title, loading_win);
   ajax_auth_mb_cache.onbeforeclick['OK'] = ajaxValidateLogin;
+  ajax_auth_mb_cache.onbeforeclick['Cancel'] = function()
+  {
+    if ( document.getElementById('autoCaptcha') )
+    {
+      var to = fly_out_top(document.getElementById('autoCaptcha'), false, true);
+      setTimeout(function() {
+          var d = document.getElementById('autoCaptcha');
+          d.parentNode.removeChild(d);
+        }, to);
+    }
+  }
   ajaxAuthLoginInnerSetup();
 }
 
@@ -334,23 +392,58 @@
           return false;
         }
         response = parseJSON(response);
+        var disable_controls = false;
+        if ( response.locked_out && !ajax_auth_error_string )
+        {
+          response.error = 'locked_out';
+          ajax_auth_error_string = ajaxAuthErrorToString(response);
+          if ( response.lockout_policy == 'captcha' )
+          {
+            ajax_auth_show_captcha = response.captcha;
+          }
+          else
+          {
+            disable_controls = true;
+          }
+        }
         var level = ajax_auth_level_cache;
         var form_html = '';
-        if ( level > USER_LEVEL_MEMBER )
+        var shown_error = false;
+        if ( ajax_auth_error_string )
+        {
+          shown_error = true;
+          form_html += '<div class="error-box-mini" id="ajax_auth_error">' + ajax_auth_error_string + '</div>';
+          ajax_auth_error_string = false;
+        }
+        else if ( level > USER_LEVEL_MEMBER )
         {
           form_html += 'Please re-enter your login details, to verify your identity.<br /><br />';
         }
+        if ( ajax_auth_show_captcha )
+         {
+           var captcha_html = ' \
+             <tr> \
+               <td>Code in image:</td> \
+               <td><input type="hidden" id="ajaxlogin_captcha_hash" value="' + ajax_auth_show_captcha + '" /><input type="text" tabindex="3" size="25" id="ajaxlogin_captcha_code" /> \
+             </tr>';
+         }
+         else
+         {
+           var captcha_html = '';
+         }
+         var disableme = ( disable_controls ) ? 'disabled="disabled" ' : '';
         form_html += ' \
           <table border="0" align="center"> \
             <tr> \
-              <td>Username:</td><td><input tabindex="1" id="ajaxlogin_user" type="text"     size="25" /> \
+              <td>Username:</td><td><input tabindex="1" id="ajaxlogin_user" type="text"     ' + disableme + 'size="25" /> \
             </tr> \
             <tr> \
-              <td>Password:</td><td><input tabindex="2" id="ajaxlogin_pass" type="password" size="25" /> \
+              <td>Password:</td><td><input tabindex="2" id="ajaxlogin_pass" type="password" ' + disableme + 'size="25" /> \
             </tr> \
+            ' + captcha_html + ' \
             <tr> \
               <td colspan="2" style="text-align: center;"> \
-                <br /><small>Trouble logging in? Try the <a href="'+makeUrlNS('Special', 'Login/' + title)+'">full login form</a>.<br />';
+                <br /><small>Trouble logging in? Try the <a href="'+makeUrlNS('Special', 'Login/' + title, 'level=' + level)+'">full login form</a>.<br />';
        if ( level <= USER_LEVEL_MEMBER )
        {
          form_html += ' \
@@ -375,8 +468,39 @@
         {
           $('ajaxlogin_user').object.focus();
         }
-        $('ajaxlogin_pass').object.onblur = function(e) { if ( !shift ) $('messageBox').object.nextSibling.firstChild.focus(); };
-        $('ajaxlogin_pass').object.onkeypress = function(e) { if ( !e && IE ) return true; if ( e.keyCode == 13 ) $('messageBox').object.nextSibling.firstChild.click(); };
+        if ( ajax_auth_show_captcha )
+        {
+          $('ajaxlogin_captcha_code').object.onblur = function(e) { if ( !shift ) $('messageBox').object.nextSibling.firstChild.focus(); };
+          $('ajaxlogin_captcha_code').object.onkeypress = function(e) { if ( !e && IE ) return true; if ( e.keyCode == 13 ) $('messageBox').object.nextSibling.firstChild.click(); };
+        }
+        else
+        {
+          $('ajaxlogin_pass').object.onblur = function(e) { if ( !shift ) $('messageBox').object.nextSibling.firstChild.focus(); };
+          $('ajaxlogin_pass').object.onkeypress = function(e) { if ( !e && IE ) return true; if ( e.keyCode == 13 ) $('messageBox').object.nextSibling.firstChild.click(); };
+        }
+        if ( disable_controls )
+        {
+          var panel = document.getElementById('messageBoxButtons');
+          panel.firstChild.disabled = true;
+        }
+        /*
+        ## This causes the background image to disappear under Fx 2
+        if ( shown_error )
+        {
+          // fade to #FFF4F4
+          var fader = new Spry.Effect.Highlight('ajax_auth_error', {duration: 1000, from: '#FFF4F4', to: '#805600', restoreColor: '#805600', finish: function()
+              {
+                var fader = new Spry.Effect.Highlight('ajax_auth_error', {duration: 3000, from: '#805600', to: '#FFF4F4', restoreColor: '#FFF4F4'});
+                fader.start();
+          }});
+          fader.start();
+        }
+        */
+        if ( ajax_auth_show_captcha )
+        {
+          ajaxShowCaptcha(ajax_auth_show_captcha);
+          ajax_auth_show_captcha = false;
+        }
       }
     });
 }
@@ -391,6 +515,15 @@
   password = document.getElementById('ajaxlogin_pass').value;
   auth_enabled = false;
   
+  if ( document.getElementById('autoCaptcha') )
+  {
+    var to = fly_out_top(document.getElementById('autoCaptcha'), false, true);
+    setTimeout(function() {
+        var d = document.getElementById('autoCaptcha');
+        d.parentNode.removeChild(d);
+      }, to);
+  }
+  
   disableJSONExts();
   
   //
@@ -446,6 +579,12 @@
     'level' : ajax_auth_level_cache
   };
   
+  if ( document.getElementById('ajaxlogin_captcha_hash') )
+  {
+    json_data.captcha_hash = document.getElementById('ajaxlogin_captcha_hash').value;
+    json_data.captcha_code = document.getElementById('ajaxlogin_captcha_code').value;
+  }
+  
   json_data = toJSONString(json_data);
   json_data = encodeURIComponent(json_data);
   
@@ -488,8 +627,25 @@
             }
             break;
           case 'error':
-            alert(response.error);
-            ajaxAuthLoginInnerSetup();
+            if ( response.data.error == 'invalid_credentials' || response.data.error == 'locked_out' )
+            {
+              ajax_auth_error_string = ajaxAuthErrorToString(response.data);
+              mb_current_obj.updateContent('');
+              document.getElementById('messageBox').style.backgroundColor = '#C0C0C0';
+              var mb_parent = document.getElementById('messageBox').parentNode;
+              new Spry.Effect.Shake(mb_parent, {duration: 1500}).start();
+              setTimeout("document.getElementById('messageBox').style.backgroundColor = '#FFF'; ajaxAuthLoginInnerSetup();", 2500);
+              
+              if ( response.data.lockout_policy == 'captcha' && response.data.error == 'locked_out' )
+              {
+                ajax_auth_show_captcha = response.captcha;
+              }
+            }
+            else
+            {
+              ajax_auth_error_string = ajaxAuthErrorToString(response.data);
+              ajaxAuthLoginInnerSetup();
+            }
             break;
           default:
             alert(ajax.responseText);
--- a/includes/comment.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/includes/comment.php	Sun Oct 07 21:31:14 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  *
  * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
--- a/includes/common.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/includes/common.php	Sun Oct 07 21:31:14 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  *
  * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
@@ -23,7 +23,7 @@
   exit;
 }
 
-$version = '1.0.2b1';
+$version = '1.1.1';
 
 function microtime_float()
 {
--- a/includes/constants.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/includes/constants.php	Sun Oct 07 21:31:14 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  * constants.php - important defines used Enano-wide
  *
@@ -37,6 +37,7 @@
 define('PAGE_GRP_CATLINK', 1);
 define('PAGE_GRP_TAGGED', 2);
 define('PAGE_GRP_NORMAL', 3);
+define('PAGE_GRP_REGEX', 4);
 
 //
 // User types - don't touch these
@@ -69,7 +70,7 @@
 define('MAX_PMS_PER_BATCH', 7); // The maximum number of users that users can send PMs to in one go; restriction does not apply to users with mod_misc rights
 define('SEARCH_RESULTS_PER_PAGE', 10);
 define('MYSQL_MAX_PACKET_SIZE', 1048576); // 1MB; this is the default in MySQL 4.x I think
-define('SEARCH_MODE', 'FULLTEXT'); // Can be FULLTEXT or BUILTIN
+define('SEARCH_MODE', 'BUILTIN'); // Can be FULLTEXT or BUILTIN
 
 // Sidebar
 
--- a/includes/dbal.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/includes/dbal.php	Sun Oct 07 21:31:14 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  *
  * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
--- a/includes/email.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/includes/email.php	Sun Oct 07 21:31:14 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  *
  * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
--- a/includes/functions.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/includes/functions.php	Sun Oct 07 21:31:14 2007 -0400
@@ -1789,6 +1789,26 @@
 
 function sanitize_html($html, $filter_php = true)
 {
+  // Random seed for substitution
+  $rand_seed = md5( sha1(microtime()) . mt_rand() );
+  
+  // Strip out comments that are already escaped
+  preg_match_all('/&lt;!--(.*?)--&gt;/', $html, $comment_match);
+  $i = 0;
+  foreach ( $comment_match[0] as $comment )
+  {
+    $html = str_replace_once($comment, "{HTMLCOMMENT:$i:$rand_seed}", $html);
+    $i++;
+  }
+  
+  // Strip out code sections that will be postprocessed by Text_Wiki
+  preg_match_all(';^<code(\s[^>]*)?>((?:(?R)|.)*?)\n</code>(\s|$);msi', $html, $code_match);
+  $i = 0;
+  foreach ( $code_match[0] as $code )
+  {
+    $html = str_replace_once($code, "{TW_CODE:$i:$rand_seed}", $html);
+    $i++;
+  }
 
   $html = preg_replace('#<([a-z]+)([\s]+)([^>]+?)'.htmlalternatives('javascript:').'(.+?)>(.*?)</\\1>#is', '&lt;\\1\\2\\3javascript:\\59&gt;\\60&lt;/\\1&gt;', $html);
   $html = preg_replace('#<([a-z]+)([\s]+)([^>]+?)'.htmlalternatives('javascript:').'(.+?)>#is', '&lt;\\1\\2\\3javascript:\\59&gt;', $html);
@@ -1802,6 +1822,8 @@
   $tag_whitelist = array_keys ( setupAttributeWhitelist() );
   if ( !$filter_php )
     $tag_whitelist[] = '?php';
+  // allow HTML comments
+  $tag_whitelist[] = '!--';
   $len = strlen($html);
   $in_quote = false;
   $quote_char = '';
@@ -1862,8 +1884,12 @@
       }
       else
       {
+        // If not filtering PHP, don't bother to strip
         if ( $tag_name == '?php' && !$filter_php )
           continue;
+        // If this is a comment, likewise skip this "tag"
+        if ( $tag_name == '!--' )
+          continue;
         $f = fixTagAttributes( $attribs_only, $tag_name );
         $s = ( empty($f) ) ? '' : ' ';
 
@@ -1891,15 +1917,28 @@
     }
 
   }
-
+  
   // Vulnerability from ha.ckers.org/xss.html:
   // <script src="http://foo.com/xss.js"
   // <
   // The rule is so specific because everything else will have been filtered by now
   $html = preg_replace('/<(script|iframe)(.+?)src=([^>]*)</i', '&lt;\\1\\2src=\\3&lt;', $html);
 
-  // Unstrip comments
-  $html = preg_replace('/&lt;!--([^>]*?)--&gt;/i', '', $html);
+  // Restore stripped comments
+  $i = 0;
+  foreach ( $comment_match[0] as $comment )
+  {
+    $html = str_replace_once("{HTMLCOMMENT:$i:$rand_seed}", $comment, $html);
+    $i++;
+  }
+  
+  // Restore stripped code
+  $i = 0;
+  foreach ( $code_match[0] as $code )
+  {
+    $html = str_replace_once("{TW_CODE:$i:$rand_seed}", $code, $html);
+    $i++;
+  }
 
   return $html;
 
@@ -2705,7 +2744,7 @@
 function sanitize_tag($tag)
 {
   $tag = strtolower($tag);
-  $tag = preg_replace('/[^\w _-]+/', '', $tag);
+  $tag = preg_replace('/[^\w _@\$%\^&-]+/', '', $tag);
   $tag = trim($tag);
   return $tag;
 }
--- a/includes/graphs.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/includes/graphs.php	Sun Oct 07 21:31:14 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  *
  * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
--- a/includes/js-compressor.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/includes/js-compressor.php	Sun Oct 07 21:31:14 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  * Javascript compression library - used to compact the client-side Javascript code (all 72KB of it!) to save some bandwidth
  *
--- a/includes/pageprocess.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/includes/pageprocess.php	Sun Oct 07 21:31:14 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * pageprocess.php - intelligent retrieval of pages
  * Copyright (C) 2006-2007 Dan Fuhry
  *
--- a/includes/pageutils.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/includes/pageutils.php	Sun Oct 07 21:31:14 2007 -0400
@@ -1,7 +1,8 @@
 <?php
+
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  * pageutils.php - a class that handles raw page manipulations, used mostly by AJAX requests or their old-fashioned form-based counterparts
  *
@@ -654,59 +655,145 @@
   function rollback($id)
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
-    if(!$session->get_permissions('history_rollback')) return('You are not authorized to perform rollbacks.');
-    if(!preg_match('#^([0-9]+)$#', (string)$id)) return('The value "id" on the query string must be an integer.');
+    if ( !$session->get_permissions('history_rollback') )
+    {
+      return('You are not authorized to perform rollbacks.');
+    }
+    if ( !preg_match('#^([0-9]+)$#', (string)$id) )
+    {
+      return('The value "id" on the query string must be an integer.');
+    }
     $e = $db->sql_query('SELECT log_type,action,date_string,page_id,namespace,page_text,char_tag,author,edit_summary FROM '.table_prefix.'logs WHERE time_id='.$id.';');
-    if(!$e) $db->_die('The rollback data could not be selected.');
+    if ( !$e )
+    {
+      $db->_die('The rollback data could not be selected.');
+    }
     $rb = $db->fetchrow();
     $db->free_result();
-    switch($rb['log_type']) {
+    
+    if ( $rb['log_type'] == 'page' && $rb['action'] != 'delete' )
+    {
+      $pagekey = $paths->nslist[$rb['namespace']] . $rb['page_id'];
+      if ( !isset($paths->pages[$pagekey]) )
+      {
+        return "Page doesn't exist";
+      }
+      $pagedata =& $paths->pages[$pagekey];
+      $protected = false;
+      // Special case: is the page protected? if so, check for even_when_protected permissions
+      if($pagedata['protected'] == 2)
+      {
+        // The page is semi-protected, determine permissions
+        if($session->user_logged_in && $session->reg_time + 60*60*24*4 < time()) 
+        {
+          $protected = false;
+        }
+        else
+        {
+          $protected = true;
+        }
+      }
+      else
+      {
+        $protected = ( $pagedata['protected'] == 1 );
+      }
+      
+      $perms = $session->fetch_page_acl($rb['page_id'], $rb['namespace']);
+      
+      if ( $protected && !$perms->get_permissions('even_when_protected') )
+      {
+        return "Because this page is protected, you need moderator rights to roll back changes.";
+      }
+    }
+    else
+    {
+      $perms =& $session;
+    }
+    
+    switch($rb['log_type'])
+    {
       case "page":
-        switch($rb['action']) {
+        switch($rb['action'])
+        {
           case "edit":
+            if ( !$perms->get_permissions('edit_page') )
+              return "You don't have permission to edit pages, so rolling back edits can't be allowed either.";
             $t = $db->escape($rb['page_text']);
             $e = $db->sql_query('UPDATE '.table_prefix.'page_text SET page_text=\''.$t.'\',char_tag=\''.$rb['char_tag'].'\' WHERE page_id=\''.$rb['page_id'].'\' AND namespace=\''.$rb['namespace'].'\'');
-            if(!$e) return("An error occurred during the rollback operation.\nMySQL said: ".mysql_error()."\n\nSQL backtrace:\n".$db->sql_backtrace());
-            else return('The page "'.$paths->pages[$paths->nslist[$rb['namespace']].$rb['page_id']]['name'].'" has been rolled back to the state it was in on '.$rb['date_string'].'.');
+            if ( !$e )
+            {
+              return("An error occurred during the rollback operation.\nMySQL said: ".mysql_error()."\n\nSQL backtrace:\n".$db->sql_backtrace());
+            }
+            else
+            {
+              return 'The page "'.$paths->pages[$paths->nslist[$rb['namespace']].$rb['page_id']]['name'].'" has been rolled back to the state it was in on '.$rb['date_string'].'.';
+            }
             break;
           case "rename":
+            if ( !$perms->get_permissions('rename') )
+              return "You don't have permission to rename pages, so rolling back renames can't be allowed either.";
             $t = $db->escape($rb['edit_summary']);
             $e = $db->sql_query('UPDATE '.table_prefix.'pages SET name=\''.$t.'\' WHERE urlname=\''.$rb['page_id'].'\' AND namespace=\''.$rb['namespace'].'\'');
-            if(!$e) return("An error occurred during the rollback operation.\nMySQL said: ".mysql_error()."\n\nSQL backtrace:\n".$db->sql_backtrace());
-            else return('The page "'.$paths->pages[$paths->nslist[$rb['namespace']].$rb['page_id']]['name'].'" has been rolled back to the name it had ("'.$rb['edit_summary'].'") before '.$rb['date_string'].'.');
+            if ( !$e )
+            {
+              return "An error occurred during the rollback operation.\nMySQL said: ".mysql_error()."\n\nSQL backtrace:\n".$db->sql_backtrace();
+            }
+            else
+            {
+              return 'The page "'.$paths->pages[$paths->nslist[$rb['namespace']].$rb['page_id']]['name'].'" has been rolled back to the name it had ("'.$rb['edit_summary'].'") before '.$rb['date_string'].'.';
+            }
             break;
           case "prot":
+            if ( !$perms->get_permissions('protect') )
+              return "You don't have permission to protect pages, so rolling back protection can't be allowed either.";
             $e = $db->sql_query('UPDATE '.table_prefix.'pages SET protected=0 WHERE urlname=\''.$rb['page_id'].'\' AND namespace=\''.$rb['namespace'].'\'');
-            if(!$e) return("An error occurred during the rollback operation.\nMySQL said: ".mysql_error()."\n\nSQL backtrace:\n".$db->sql_backtrace());
-            else return('The page "'.$paths->pages[$paths->nslist[$rb['namespace']].$rb['page_id']]['name'].'" has been unprotected according to the log created at '.$rb['date_string'].'.');
+            if ( !$e )
+              return "An error occurred during the rollback operation.\nMySQL said: ".mysql_error()."\n\nSQL backtrace:\n".$db->sql_backtrace();
+            else
+              return 'The page "'.$paths->pages[$paths->nslist[$rb['namespace']].$rb['page_id']]['name'].'" has been unprotected according to the log created at '.$rb['date_string'].'.';
             break;
           case "semiprot":
+            if ( !$perms->get_permissions('protect') )
+              return "You don't have permission to protect pages, so rolling back protection can't be allowed either.";
             $e = $db->sql_query('UPDATE '.table_prefix.'pages SET protected=0 WHERE urlname=\''.$rb['page_id'].'\' AND namespace=\''.$rb['namespace'].'\'');
-            if(!$e) return("An error occurred during the rollback operation.\nMySQL said: ".mysql_error()."\n\nSQL backtrace:\n".$db->sql_backtrace());
-            else return('The page "'.$paths->pages[$paths->nslist[$rb['namespace']].$rb['page_id']]['name'].'" has been unprotected according to the log created at '.$rb['date_string'].'.');
+            if ( !$e )
+              return "An error occurred during the rollback operation.\nMySQL said: ".mysql_error()."\n\nSQL backtrace:\n".$db->sql_backtrace();
+            else
+              return 'The page "'.$paths->pages[$paths->nslist[$rb['namespace']].$rb['page_id']]['name'].'" has been unprotected according to the log created at '.$rb['date_string'].'.';
             break;
           case "unprot":
+            if ( !$perms->get_permissions('protect') )
+              return "You don't have permission to protect pages, so rolling back protection can't be allowed either.";
             $e = $db->sql_query('UPDATE '.table_prefix.'pages SET protected=1 WHERE urlname=\''.$rb['page_id'].'\' AND namespace=\''.$rb['namespace'].'\'');
-            if(!$e) return("An error occurred during the rollback operation.\nMySQL said: ".mysql_error()."\n\nSQL backtrace:\n".$db->sql_backtrace());
-            else return('The page "'.$paths->pages[$paths->nslist[$rb['namespace']].$rb['page_id']]['name'].'" has been protected according to the log created at '.$rb['date_string'].'.');
+            if ( !$e )
+              return "An error occurred during the rollback operation.\nMySQL said: ".mysql_error()."\n\nSQL backtrace:\n".$db->sql_backtrace();
+            else
+              return 'The page "'.$paths->pages[$paths->nslist[$rb['namespace']].$rb['page_id']]['name'].'" has been protected according to the log created at '.$rb['date_string'].'.';
             break;
           case "delete":
-            if(!$session->get_permissions('history_rollback_extra')) return('Administrative privileges are required for page undeletion.');
-            if(isset($paths->pages[$paths->cpage['urlname']])) return('You cannot raise a dead page that is alive.');
+            if ( !$perms->get_permissions('history_rollback_extra') )
+              return 'Administrative privileges are required for page undeletion.';
+            if ( isset($paths->pages[$paths->cpage['urlname']]) )
+              return 'You cannot raise a dead page that is alive.';
             $name = str_replace('_', ' ', $rb['page_id']);
             $e = $db->sql_query('INSERT INTO '.table_prefix.'pages(name,urlname,namespace) VALUES( \''.$name.'\', \''.$rb['page_id'].'\',\''.$rb['namespace'].'\' )');if(!$e) return("An error occurred during the rollback operation.\nMySQL said: ".mysql_error()."\n\nSQL backtrace:\n".$db->sql_backtrace());
             $e = $db->sql_query('SELECT page_text,char_tag FROM '.table_prefix.'logs WHERE page_id=\''.$rb['page_id'].'\' AND namespace=\''.$rb['namespace'].'\' AND log_type=\'page\' AND action=\'edit\' ORDER BY time_id DESC;'); if(!$e) return("An error occurred during the rollback operation.\nMySQL said: ".mysql_error()."\n\nSQL backtrace:\n".$db->sql_backtrace());
             $r = $db->fetchrow();
             $e = $db->sql_query('INSERT INTO '.table_prefix.'page_text(page_id,namespace,page_text,char_tag) VALUES(\''.$rb['page_id'].'\',\''.$rb['namespace'].'\',\''.$db->escape($r['page_text']).'\',\''.$r['char_tag'].'\')'); if(!$e) return("An error occurred during the rollback operation.\nMySQL said: ".mysql_error()."\n\nSQL backtrace:\n".$db->sql_backtrace());
-            return('The page "'.$name.'" has been undeleted according to the log created at '.$rb['date_string'].'.');
+            return 'The page "'.$name.'" has been undeleted according to the log created at '.$rb['date_string'].'.';
             break;
           case "reupload":
-            if(!$session->get_permissions('history_rollbacks_extra')) return('Administrative privileges are required for file rollbacks.');
+            if ( !$session->get_permissions('history_rollbacks_extra') )
+            {
+              return 'Administrative privileges are required for file rollbacks.';
+            }
             $newtime = time();
             $newdate = date('d M Y h:i a');
-            if(!$db->sql_query('UPDATE '.table_prefix.'logs SET time_id='.$newtime.',date_string=\''.$newdate.'\' WHERE time_id='.$id)) return('Error during query: '.mysql_error());
-            if(!$db->sql_query('UPDATE '.table_prefix.'files SET time_id='.$newtime.' WHERE time_id='.$id)) return('Error during query: '.mysql_error());
-            return('The file has been rolled back to the version uploaded on '.date('d M Y h:i a', (int)$id).'.');
+            if(!$db->sql_query('UPDATE '.table_prefix.'logs SET time_id='.$newtime.',date_string=\''.$newdate.'\' WHERE time_id='.$id))
+              return 'Error during query: '.mysql_error();
+            if(!$db->sql_query('UPDATE '.table_prefix.'files SET time_id='.$newtime.' WHERE time_id='.$id))
+              return 'Error during query: '.mysql_error();
+            return 'The file has been rolled back to the version uploaded on '.date('d M Y h:i a', (int)$id).'.';
             break;
           default:
             return('Rollback of the action "'.$rb['action'].'" is not yet supported.');
--- a/includes/paths.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/includes/paths.php	Sun Oct 07 21:31:14 2007 -0400
@@ -2,7 +2,7 @@
 
 /**
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  * paths.php - The part of Enano that actually manages content. Everything related to page handling and namespaces is in here.
  *
@@ -855,14 +855,35 @@
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     
-    $page_id = $db->escape(sanitize_page_id($page_id));
+    static $cache = array();
+    
+    if ( count($cache) == 0 )
+    {
+      foreach ( $this->nslist as $key => $_ )
+      {
+        $cache[$key] = array();
+      }
+    }
+    
     if ( !isset($this->nslist[$namespace]) )
       die('$paths->get_page_groups(): HACKING ATTEMPT: namespace "'. htmlspecialchars($namespace) .'" doesn\'t exist');
     
+    $page_id_unescaped = $paths->nslist[$namespace] .
+                         dirtify_page_id($page_id);
+    $page_id_str       = $paths->nslist[$namespace] .
+                         sanitize_page_id($page_id);
+    
+    $page_id = $db->escape(sanitize_page_id($page_id));
+    
+    if ( isset($cache[$namespace][$page_id]) )
+    {
+      return $cache[$namespace][$page_id];
+    }
+    
     $group_list = array();
     
     // What linked categories have this page?
-    $q = $db->sql_query('SELECT g.pg_id FROM '.table_prefix.'page_groups AS g
+    $q = $db->sql_unbuffered_query('SELECT g.pg_id, g.pg_type, g.pg_target FROM '.table_prefix.'page_groups AS g
   LEFT JOIN '.table_prefix.'categories AS c
     ON ( ( c.category_id = g.pg_target AND g.pg_type = ' . PAGE_GRP_CATLINK . ' ) OR c.category_id IS NULL )
   LEFT JOIN '.table_prefix.'page_group_members AS m
@@ -872,47 +893,32 @@
   WHERE
     ( c.page_id=\'' . $page_id . '\' AND c.namespace=\'' . $namespace . '\' ) OR
     ( t.page_id=\'' . $page_id . '\' AND t.namespace=\'' . $namespace . '\' ) OR
-    ( m.page_id=\'' . $page_id . '\' AND m.namespace=\'' . $namespace . '\' );');
+    ( m.page_id=\'' . $page_id . '\' AND m.namespace=\'' . $namespace . '\' ) OR
+    ( g.pg_type = ' . PAGE_GRP_REGEX . ' );');
     if ( !$q )
       $db->_die();
     
     while ( $row = $db->fetchrow() )
     {
-      $group_list[] = $row['pg_id'];
+      if ( $row['pg_type'] == PAGE_GRP_REGEX )
+      {
+        //echo "&lt;debug&gt; matching page " . htmlspecialchars($page_id_unescaped) . " against regex <tt>" . htmlspecialchars($row['pg_target']) . "</tt>.";
+        if ( @preg_match($row['pg_target'], $page_id_unescaped) || @preg_match($row['pg_target'], $page_id_str) )
+        {
+          //echo "..matched";
+          $group_list[] = $row['pg_id'];
+        }
+        //echo "<br />";
+      }
+      else
+      {
+        $group_list[] = $row['pg_id'];
+      }
     }
     
     $db->free_result();
     
-    /*
-    // Static-page groups
-    $q = $db->sql_query('SELECT g.pg_id FROM '.table_prefix.'page_groups AS g
-                           LEFT JOIN '.table_prefix.'page_group_members AS m
-                             ON ( g.pg_id = m.pg_id )
-                           WHERE m.page_id=\'' . $page_id . '\' AND m.namespace=\'' . $namespace . '\'
-                           GROUP BY g.pg_id;');
-    
-    if ( !$q )
-      $db->_die();
-    
-    while ( $row = $db->fetchrow() )
-    {
-      $group_list[] = $row['pg_id'];
-    }
-    
-    // Tag groups
-    
-    $q = $db->sql_query('SELECT g.pg_id FROM '.table_prefix.'page_groups AS g
-                           LEFT JOIN '.table_prefix.'tags AS t
-                             ON ( t.tag_name = g.pg_target AND pg_type = ' . PAGE_GRP_TAGGED . ' )
-                           WHERE t.page_id = \'' . $page_id . '\' AND t.namespace = \'' . $namespace . '\';');
-    if ( !$q )
-      $db->_die();
-    
-    while ( $row = $db->fetchrow() )
-    {
-      $group_list[] = $row['pg_id'];
-    }
-    */
+    $cache[$namespace][$page_id] = $group_list;
     
     return $group_list;
     
--- a/includes/plugins.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/includes/plugins.php	Sun Oct 07 21:31:14 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  *
  * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
--- a/includes/render.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/includes/render.php	Sun Oct 07 21:31:14 2007 -0400
@@ -248,6 +248,12 @@
       $text = preg_replace('/<nodisplay>(.*?)<\/nodisplay>/is', '', $text);
     }
     
+    $code = $plugins->setHook('render_wikiformat_pre');
+    foreach ( $code as $cmd )
+    {
+      eval($cmd);
+    }
+    
     if ( !$plaintext )
     {
       // Process images
@@ -264,7 +270,8 @@
       }
     }
     
-    $template_regex = "/\{\{([^\]]+?)((\n([ ]*?)[A-z0-9]+([ ]*?)=([ ]*?)(.+?))*)\}\}/is";
+    //$template_regex = "/\{\{([^\]]+?)((\n([ ]*?)[A-z0-9]+([ ]*?)=([ ]*?)(.+?))*)\}\}/is";
+    $template_regex = "/\{\{(.+)((\n|\|[ ]*([A-z0-9]+)[ ]*=[ ]*(.+))*)\}\}/isU";
     $i = 0;
     while ( preg_match($template_regex, $text) )
     {
@@ -290,10 +297,26 @@
       $result = $wiki->transform($text, 'Xhtml');
     }
     
-    // if ( !$plaintext )
-    // {
-    //   $result = RenderMan::process_imgtags_stage2($result, $taglist);
-    // }
+    // HTML fixes
+    $result = preg_replace('#<tr>([\s]*?)<\/tr>#is', '', $result);
+    $result = preg_replace('#<p>([\s]*?)<\/p>#is', '', $result);
+    $result = preg_replace('#<br />([\s]*?)<table#is', '<table', $result);
+    $result = str_replace("<pre><code>\n", "<pre><code>", $result);
+    $result = preg_replace("/<p><table([^>]*?)><\/p>/", "<table\\1>", $result);
+    $result = str_replace("<br />\n</td>", "\n</td>", $result);
+    $result = str_replace("<p><tr>", "<tr>", $result);
+    $result = str_replace("<tr><br />", "<tr>", $result);
+    $result = str_replace("</tr><br />", "</tr>", $result);
+    $result = str_replace("</table><br />", "</table>", $result);
+    $result = preg_replace('/<\/table>$/', "</table><br /><br />", $result);
+    $result = str_replace("<p></div></p>", "</div>", $result);
+    $result = str_replace("<p></table></p>", "</table>", $result);
+    
+    $code = $plugins->setHook('render_wikiformat_post');
+    foreach ( $code as $cmd )
+    {
+      eval($cmd);
+    }
     
     // Reinsert <nowiki> sections
     for($i=0;$i<$nw;$i++)
@@ -311,7 +334,8 @@
     
   }
   
-  function wikiFormat($message, $filter_links = true, $do_params = false, $plaintext = false) {
+  function wikiFormat($message, $filter_links = true, $do_params = false, $plaintext = false)
+  {
     global $db, $session, $paths, $template, $plugins; // Common objects
     
     return RenderMan::next_gen_wiki_format($message, $plaintext, $filter_links, $do_params);
@@ -384,6 +408,8 @@
     $result = str_replace("</table></p>", "</table>", $result);
     $result = str_replace("</table><br />", "</table>", $result);
     $result = preg_replace('/<\/table>$/', "</table><br /><br />", $result);
+    $result = str_replace("<p></div></p>", "</div>", $result);
+    $result = str_replace("<p></table></p>", "</table>", $result);
     
     $result = str_replace('<nowiki>',  '&lt;nowiki&gt;',  $result);
     $result = str_replace('</nowiki>', '&lt;/nowiki&gt;', $result);
@@ -460,8 +486,8 @@
       list($page_id, $namespace) = RenderMan::strToPageID($matches[1][$i]);
       $pid_clean = $paths->nslist[$namespace] . sanitize_page_id($page_id);
       
-      $url = makeUrl($matches[1][$i], false, true);
-      $inner_text = htmlspecialchars(get_page_title($pid_clean));
+      $url = makeUrl($pid_clean, false, true);
+      $inner_text = ( isPage($pid_clean) ) ? htmlspecialchars(get_page_title($pid_clean)) : htmlspecialchars($matches[1][$i]);
       $quot = '"';
       $exists = ( isPage($pid_clean) ) ? '' : ' class="wikilink-nonexistent"';
       
@@ -473,46 +499,6 @@
     return $text;
   }
   
-  /* *
-   * Replaces template inclusions with the templates
-   * @param string $message The text to format
-   * @return string
-   * /
-   
-  function old_include_templates($message)
-  {
-    $random_id = md5( time() . mt_rand() );
-    preg_match_all('#\{\{(.+?)\}\}#s', $message, $matchlist);
-    foreach($matchlist[1] as $m)
-    {
-      $mn = $m;
-      // Strip out wikilinks and re-add them after the explosion (because of the "|")
-      preg_match_all('#\[\[(.+?)\]\]#i', $m, $linklist);
-      //echo '<pre>'.print_r($linklist, true).'</pre>';
-      for($i=0;$i<sizeof($linklist[1]);$i++)
-      {
-        $mn = str_replace('[['.$linklist[1][$i].']]', '{WIKILINK:'.$random_id.':'.$i.'}', $mn);
-      }
-      
-      $ar = explode('|', $mn);
-      
-      for($j=0;$j<sizeof($ar);$j++)
-      {
-        for($i=0;$i<sizeof($linklist[1]);$i++)
-        {
-          $ar[$j] = str_replace('{WIKILINK:'.$random_id.':'.$i.'}', '[['.$linklist[1][$i].']]', $ar[$j]);
-        }
-      }
-      
-      $tp = $ar[0];
-      unset($ar[0]);
-      $tp = str_replace(' ', '_', $tp);
-      $message = str_replace('{{'.$m.'}}', RenderMan::getTemplate($tp, $ar), $message);
-    }
-    return $message;
-  }
-  */
-  
   /**
    * Parses a partial template tag in wikitext, and return an array with the parameters.
    * @param string The portion of the template tag that contains the parameters.
@@ -528,16 +514,26 @@
   
   function parse_template_vars($input)
   {
-    $input = explode("\n", trim( $input ));
+    if ( !preg_match('/^(\|[ ]*([A-z0-9_]+)([ ]*)=([ ]*)(.+?))*$/is', trim($input)) )
+    {
+      $using_pipes = false;
+      $input = explode("\n", trim( $input ));
+    }
+    else
+    {
+      $using_pipes = true;
+      $input = substr($input, 1);
+      $input = explode("|", trim( $input ));
+    }
     $parms = Array();
     $current_line = '';
     $current_parm = '';
     foreach ( $input as $num => $line )
     {
-      if ( preg_match('/^([ ]*?)([A-z0-9_]+?)([ ]*?)=([ ]*?)(.+?)$/i', $line, $matches) )
+      if ( preg_match('/^[ ]*([A-z0-9_]+)([ ]*)=([ ]*)(.+?)$/is', $line, $matches) )
       {
-        $parm =& $matches[2];
-        $text =& $matches[5];
+        $parm =& $matches[1];
+        $text =& $matches[4];
         if ( $parm == $current_parm )
         {
           $current_line .= $text;
@@ -570,6 +566,7 @@
   
   /**
    * Processes all template tags within a block of wikitext.
+   * Updated in 1.0.2 to also parse template tags in the format of {{Foo |a = b |b = c |c = therefore, a}}
    * @param string The text to process
    * @return string Formatted text
    * @example
@@ -578,16 +575,18 @@
      parm1 = Foo
      parm2 = Bar
      }}';
-   $text = include_templates($text);
+   $text = RenderMan::include_templates($text);
    * </code>
    */
   
   function include_templates($text)
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
-    $template_regex = "/\{\{([^\]]+?)((\n([ ]*?)[A-z0-9]+([ ]*?)=([ ]*?)(.+?))*)\}\}/is";
+    // $template_regex = "/\{\{([^\]]+?)((\n([ ]*?)[A-z0-9]+([ ]*?)=([ ]*?)(.+?))*)\}\}/is";
+    $template_regex = "/\{\{(.+)(((\n|[ ]*\|)[ ]*([A-z0-9]+)[ ]*=[ ]*(.+))*)\}\}/isU";
     if ( $count = preg_match_all($template_regex, $text, $matches) )
     {
+      //die('<pre>' . print_r($matches, true) . '</pre>');
       for ( $i = 0; $i < $count; $i++ )
       {
         $matches[1][$i] = sanitize_page_id($matches[1][$i]);
@@ -595,10 +594,9 @@
         if ( !empty($parmsection) )
         {
           $parms = RenderMan::parse_template_vars($parmsection);
-          foreach ( $parms as $j => $parm )
-          {
-            $parms[$j] = $parm;
-          }
+          if ( !is_array($parms) )
+            // Syntax error
+            $parms = array();
         }
         else
         {
--- a/includes/search.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/includes/search.php	Sun Oct 07 21:31:14 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  * search.php - algorithm used to search pages
  *
--- a/includes/sessions.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/includes/sessions.php	Sun Oct 07 21:31:14 2007 -0400
@@ -547,15 +547,52 @@
    * @param string $aes_key The MD5 hash of the encryption key, hex-encoded
    * @param string $challenge The 256-bit MD5 challenge string - first 128 bits should be the hash, the last 128 should be the challenge salt
    * @param int $level The privilege level we're authenticating for, defaults to 0
+   * @param array $captcha_hash Optional. If we're locked out and the lockout policy is captcha, this should be the identifier for the code.
+   * @param array $captcha_code Optional. If we're locked out and the lockout policy is captcha, this should be the code the user entered.
    * @return string 'success' on success, or error string on failure
    */
    
-  function login_with_crypto($username, $aes_data, $aes_key, $challenge, $level = USER_LEVEL_MEMBER)
+  function login_with_crypto($username, $aes_data, $aes_key, $challenge, $level = USER_LEVEL_MEMBER, $captcha_hash = false, $captcha_code = false)
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     
     $privcache = $this->private_key;
     
+    // Lockout stuff
+    $threshold = ( $_ = getConfig('lockout_threshold') ) ? intval($_) : 5;
+    $duration  = ( $_ = getConfig('lockout_duration') ) ? intval($_) : 15;
+    // convert to minutes
+    $duration  = $duration * 60;
+    $policy = ( $x = getConfig('lockout_policy') && in_array(getConfig('lockout_policy'), array('lockout', 'disable', 'captcha')) ) ? getConfig('lockout_policy') : 'lockout';
+    if ( $policy == 'captcha' && $captcha_hash && $captcha_code )
+    {
+      // policy is captcha -- check if it's correct, and if so, bypass lockout check
+      $real_code = $this->get_captcha($captcha_hash);
+    }
+    if ( $policy != 'disable' && !( $policy == 'captcha' && isset($real_code) && $real_code == $captcha_code ) )
+    {
+      $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']);
+      $timestamp_cutoff = time() - $duration;
+      $q = $this->sql('SELECT timestamp FROM '.table_prefix.'lockout WHERE timestamp > ' . $timestamp_cutoff . ' AND ipaddr = \'' . $ipaddr . '\' ORDER BY timestamp DESC;');
+      $fails = $db->numrows();
+      if ( $fails > $threshold )
+      {
+        // ooh boy, somebody's in trouble ;-)
+        $row = $db->fetchrow();
+        $db->free_result();
+        return array(
+            'success' => false,
+            'error' => 'locked_out',
+            'lockout_threshold' => $threshold,
+            'lockout_duration' => ( $duration / 60 ),
+            'lockout_fails' => $fails,
+            'lockout_policy' => $policy,
+            'lockout_last_time' => $row['timestamp']
+          );
+      }
+      $db->free_result();
+    }
+    
     // Instanciate the Rijndael encryption object
     $aes = new AESCrypt(AES_BITS, AES_BLOCKSIZE);
     
@@ -563,13 +600,19 @@
     
     $aes_key = $this->fetch_public_key($aes_key);
     if(!$aes_key)
-      return 'Couldn\'t look up public key "'.$aes_key.'" for decryption';
+      return array(
+        'success' => false,
+        'error' => 'key_not_found'
+        );
     
     // Convert the key to a binary string
     $bin_key = hexdecode($aes_key);
     
     if(strlen($bin_key) != AES_BITS / 8)
-      return 'The decryption key is the wrong length';
+      return array(
+        'success' => false,
+        'error' => 'key_wrong_length'
+        );
     
     // Decrypt our password
     $password = $aes->decrypt($aes_data, $bin_key, ENC_HEX);
@@ -585,13 +628,33 @@
     $this->sql('SELECT password,old_encryption,user_id,user_level,theme,style,temp_password,temp_password_time FROM '.table_prefix.'users WHERE lcase(username)=\''.$db_username_lower.'\' OR username=\'' . $db_username . '\';');
     if($db->numrows() < 1)
     {
-      return "The username and/or password is incorrect.\n$db->latest_query";
       // This wasn't logged in <1.0.2, dunno how it slipped through
       if($level > USER_LEVEL_MEMBER)
         $this->sql('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,date_string,author,edit_summary,page_text) VALUES(\'security\', \'admin_auth_bad\', '.time().', \''.date('d M Y h:i a').'\', \''.$db->escape($username).'\', \''.$db->escape($_SERVER['REMOTE_ADDR']).'\', ' . intval($level) . ')');
       else
         $this->sql('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,date_string,author,edit_summary) VALUES(\'security\', \'auth_bad\', '.time().', \''.date('d M Y h:i a').'\', \''.$db->escape($username).'\', \''.$db->escape($_SERVER['REMOTE_ADDR']).'\')');
-        
+      
+      if ( $policy != 'disable' )
+      {
+        $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']);
+        // increment fail count
+        $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action) VALUES(\'' . $ipaddr . '\', UNIX_TIMESTAMP(), \'credential\');');
+        $fails++;
+        // ooh boy, somebody's in trouble ;-)
+        return array(
+            'success' => false,
+            'error' => ( $fails >= $threshold ) ? 'locked_out' : 'invalid_credentials',
+            'lockout_threshold' => $threshold,
+            'lockout_duration' => ( $duration / 60 ),
+            'lockout_fails' => $fails,
+            'lockout_policy' => $policy
+          );
+      }
+      
+      return array(
+          'success' => false,
+          'error' => 'invalid_credentials'
+        );
     }
     $row = $db->fetchrow();
     
@@ -642,7 +705,10 @@
     if($success)
     {
       if($level > $row['user_level'])
-        return 'You are not authorized for this level of access.';
+        return array(
+          'success' => false,
+          'error' => 'too_big_for_britches'
+        );
       
       $sess = $this->register_session(intval($row['user_id']), $username, $password, $level);
       if($sess)
@@ -662,10 +728,15 @@
         {
           eval($cmd);
         }
-        return 'success';
+        return array(
+          'success' => true
+        );
       }
       else
-        return 'Your login credentials were correct, but an internal error occurred while registering the session key in the database.';
+        return array(
+          'success' => false,
+          'error' => 'backend_fail'
+        );
     }
     else
     {
@@ -674,7 +745,27 @@
       else
         $this->sql('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,date_string,author,edit_summary) VALUES(\'security\', \'auth_bad\', '.time().', \''.date('d M Y h:i a').'\', \''.$db->escape($username).'\', \''.$db->escape($_SERVER['REMOTE_ADDR']).'\')');
         
-      return 'The username and/or password is incorrect.';
+      // Do we also need to increment the lockout countdown?
+      if ( $policy != 'disable' )
+      {
+        $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']);
+        // increment fail count
+        $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action) VALUES(\'' . $ipaddr . '\', UNIX_TIMESTAMP(), \'credential\');');
+        $fails++;
+        return array(
+            'success' => false,
+            'error' => ( $fails >= $threshold ) ? 'locked_out' : 'invalid_credentials',
+            'lockout_threshold' => $threshold,
+            'lockout_duration' => ( $duration / 60 ),
+            'lockout_fails' => $fails,
+            'lockout_policy' => $policy
+          );
+      }
+        
+      return array(
+        'success' => false,
+        'error' => 'invalid_credentials'
+      );
     }
   }
   
@@ -700,6 +791,41 @@
       return $this->login_compat($username, $pass_hashed, $level);
     }
     
+    // Lockout stuff
+    $threshold = ( $_ = getConfig('lockout_threshold') ) ? intval($_) : 5;
+    $duration  = ( $_ = getConfig('lockout_duration') ) ? intval($_) : 15;
+    // convert to minutes
+    $duration  = $duration * 60;
+    $policy = ( $x = getConfig('lockout_policy') && in_array(getConfig('lockout_policy'), array('lockout', 'disable', 'captcha')) ) ? getConfig('lockout_policy') : 'lockout';
+    if ( $policy == 'captcha' && $captcha_hash && $captcha_code )
+    {
+      // policy is captcha -- check if it's correct, and if so, bypass lockout check
+      $real_code = $this->get_captcha($captcha_hash);
+    }
+    if ( $policy != 'disable' && !( $policy == 'captcha' && isset($real_code) && $real_code == $captcha_code ) )
+    {
+      $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']);
+      $timestamp_cutoff = time() - $duration;
+      $q = $this->sql('SELECT timestamp FROM '.table_prefix.'lockout WHERE timestamp > ' . $timestamp_cutoff . ' AND ipaddr = \'' . $ipaddr . '\' ORDER BY timestamp DESC;');
+      $fails = $db->numrows();
+      if ( $fails > $threshold )
+      {
+        // ooh boy, somebody's in trouble ;-)
+        $row = $db->fetchrow();
+        $db->free_result();
+        return array(
+            'success' => false,
+            'error' => 'locked_out',
+            'lockout_threshold' => $threshold,
+            'lockout_duration' => ( $duration / 60 ),
+            'lockout_fails' => $fails,
+            'lockout_policy' => $policy,
+            'lockout_last_time' => $row['timestamp']
+          );
+      }
+      $db->free_result();
+    }
+    
     // Instanciate the Rijndael encryption object
     $aes = new AESCrypt(AES_BITS, AES_BLOCKSIZE);
     
@@ -709,7 +835,35 @@
     // Retrieve the real password from the database
     $this->sql('SELECT password,old_encryption,user_id,user_level,temp_password,temp_password_time FROM '.table_prefix.'users WHERE lcase(username)=\''.$this->prepare_text(strtolower($username)).'\';');
     if($db->numrows() < 1)
-      return 'The username and/or password is incorrect.';
+    {
+      // This wasn't logged in <1.0.2, dunno how it slipped through
+      if($level > USER_LEVEL_MEMBER)
+        $this->sql('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,date_string,author,edit_summary,page_text) VALUES(\'security\', \'admin_auth_bad\', '.time().', \''.date('d M Y h:i a').'\', \''.$db->escape($username).'\', \''.$db->escape($_SERVER['REMOTE_ADDR']).'\', ' . intval($level) . ')');
+      else
+        $this->sql('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,date_string,author,edit_summary) VALUES(\'security\', \'auth_bad\', '.time().', \''.date('d M Y h:i a').'\', \''.$db->escape($username).'\', \''.$db->escape($_SERVER['REMOTE_ADDR']).'\')');
+      
+      // Do we also need to increment the lockout countdown?
+      if ( $policy != 'disable' )
+      {
+        $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']);
+        // increment fail count
+        $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action) VALUES(\'' . $ipaddr . '\', UNIX_TIMESTAMP(), \'credential\');');
+        $fails++;
+        return array(
+            'success' => false,
+            'error' => ( $fails >= $threshold ) ? 'locked_out' : 'invalid_credentials',
+            'lockout_threshold' => $threshold,
+            'lockout_duration' => ( $duration / 60 ),
+            'lockout_fails' => $fails,
+            'lockout_policy' => $policy
+          );
+      }
+      
+      return array(
+        'success' => false,
+        'error' => 'invalid_credentials'
+      );
+    }
     $row = $db->fetchrow();
     
     // Check to see if we're logging in using a temporary password
@@ -758,7 +912,10 @@
     if($success)
     {
       if((int)$level > (int)$row['user_level'])
-        return 'You are not authorized for this level of access.';
+        return array(
+          'success' => false,
+          'error' => 'too_big_for_britches'
+        );
       $sess = $this->register_session(intval($row['user_id']), $username, $real_pass, $level);
       if($sess)
       {
@@ -773,10 +930,15 @@
           eval($cmd);
         }
         
-        return 'success';
+        return array(
+          'success' => true
+          );
       }
       else
-        return 'Your login credentials were correct, but an internal error occured while registering the session key in the database.';
+        return array(
+          'success' => false,
+          'error' => 'backend_fail'
+        );
     }
     else
     {
@@ -785,7 +947,27 @@
       else
         $this->sql('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,date_string,author,edit_summary) VALUES(\'security\', \'auth_bad\', '.time().', \''.date('d M Y h:i a').'\', \''.$db->escape($username).'\', \''.$db->escape($_SERVER['REMOTE_ADDR']).'\')');
         
-      return 'The username and/or password is incorrect.';
+      // Do we also need to increment the lockout countdown?
+      if ( $policy != 'disable' )
+      {
+        $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']);
+        // increment fail count
+        $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action) VALUES(\'' . $ipaddr . '\', UNIX_TIMESTAMP(), \'credential\');');
+        $fails++;
+        return array(
+            'success' => false,
+            'error' => ( $fails >= $threshold ) ? 'locked_out' : 'invalid_credentials',
+            'lockout_threshold' => $threshold,
+            'lockout_duration' => ( $duration / 60 ),
+            'lockout_fails' => $fails,
+            'lockout_policy' => $policy
+          );
+      }
+        
+      return array(
+        'success' => false,
+        'error' => 'invalid_credentials'
+      );
     }
   }
   
@@ -2117,13 +2299,30 @@
       return false;
     }
     
+    // cache of permission objects (to save RAM and SQL queries)
+    static $objcache = array();
+    
+    if ( count($objcache) == 0 )
+    {
+      foreach ( $paths->nslist as $key => $_ )
+      {
+        $objcache[$key] = array();
+      }
+    }
+    
+    if ( isset($objcache[$namespace][$page_id]) )
+    {
+      return $objcache[$namespace][$page_id];
+    }
+    
     //if ( !isset( $paths->pages[$paths->nslist[$namespace] . $page_id] ) )
     //{
     //  // Page does not exist
     //  return false;
     //}
     
-    $object = new Session_ACLPageInfo( $page_id, $namespace, $this->acl_types, $this->acl_descs, $this->acl_deps, $this->acl_base_cache );
+    $objcache[$namespace][$page_id] = new Session_ACLPageInfo( $page_id, $namespace, $this->acl_types, $this->acl_descs, $this->acl_deps, $this->acl_base_cache );
+    $object =& $objcache[$namespace][$page_id];
     
     return $object;
     
--- a/includes/stats.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/includes/stats.php	Sun Oct 07 21:31:14 2007 -0400
@@ -1,7 +1,8 @@
 <?php
+
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  * stats.php - handles statistics for pages (disablable in the admin CP)
  *
--- a/includes/tagcloud.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/includes/tagcloud.php	Sun Oct 07 21:31:14 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  *
  * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
--- a/includes/template.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/includes/template.php	Sun Oct 07 21:31:14 2007 -0400
@@ -625,8 +625,7 @@
     
     $this->tpl_bool['stupid_mode'] = false;
     
-    if($paths->page == $paths->nslist['Special'].'Administration') $this->tpl_bool['in_admin'] = true;
-    else $this->tpl_bool['in_admin'] = false;
+    $this->tpl_bool['in_admin'] = ( ( $paths->cpage['urlname_nons'] == 'Administration' && $paths->namespace == 'Special' ) || $paths->namespace == 'Admin' );
     
     $p = ( isset($_GET['printable']) ) ? '/printable' : '';
     
@@ -784,7 +783,13 @@
     dc_here('template: generating and sending the page header');
     if(!defined('ENANO_HEADERS_SENT'))
       define('ENANO_HEADERS_SENT', '');
-    if(!$this->no_headers) echo ( $simple ) ? $this->process_template('simple-header.tpl') : $this->process_template('header.tpl');
+    if ( !$this->no_headers )
+    {
+      $header = ( $simple ) ?
+        $this->process_template('simple-header.tpl') :
+        $this->process_template('header.tpl');
+      echo $header;
+    }
     if ( !$simple && $session->user_logged_in && $session->unread_pms > 0 )
     {
       echo $this->notify_unread_pms();
@@ -871,78 +876,232 @@
     else return '';
   }
   
-  function process_template($file) {
+  /**
+   * Compiles and executes a template based on the current variables and booleans. Loads
+   * the theme and initializes variables if needed. This mostly just calls child functions.
+   * @param string File to process
+   * @return string
+   */
+  
+  function process_template($file)
+  {
     global $db, $session, $paths, $template, $plugins; // Common objects
     if(!defined('ENANO_TEMPLATE_LOADED'))
     {
       $this->load_theme();
       $this->init_vars();
     }
-    eval($this->compile_template($file));
-    return $tpl_code;
+    
+    $compiled = $this->compile_template($file);
+    return eval($compiled);
   }
   
-  function extract_vars($file) {
+  /**
+   * Loads variables from the specified template file. Returns an associative array containing the variables.
+   * @param string Template file to process (elements.tpl)
+   * @return array
+   */
+  
+  function extract_vars($file)
+  {
     global $db, $session, $paths, $template, $plugins; // Common objects
-    if(!$this->theme)
+    
+    // Sometimes this function gets called before the theme is loaded
+    // This is a bad coding practice so this function will always be picky.
+    if ( !$this->theme )
     {
       die('$template->extract_vars(): theme not yet loaded, so we can\'t open template files yet...this is a bug and should be reported.<br /><br />Backtrace, most recent call first:<pre>'.enano_debug_print_backtrace(true).'</pre>');
     }
-    if(!is_file(ENANO_ROOT . '/themes/'.$this->theme.'/'.$file)) die('Cannot find '.$file.' file for style "'.$this->theme.'", exiting');
-    $text = file_get_contents(ENANO_ROOT . '/themes/'.$this->theme.'/'.$file);
+    
+    // Full pathname of template file
+    $tpl_file_fullpath = ENANO_ROOT . '/themes/' . $this->theme . '/' . $file;
+    
+    // Make sure the template even exists
+    if ( !is_file($tpl_file_fullpath) )
+    {
+      die_semicritical('Cannot find template file',
+                       '<p>The template parser was asked to load the file "' . htmlspecialchars($filename) . '", but that file couldn\'t be found in the directory for
+                           the current theme.</p>
+                        <p>Additional debugging information:<br />
+                           <b>Theme currently in use: </b>' . $this->theme . '<br />
+                           <b>Requested file: </b>' . $file . '
+                           </p>');
+    }
+    // Retrieve file contents
+    $text = file_get_contents($tpl_file_fullpath);
+    if ( !$text )
+    {
+      return false;
+    }
+    
+    // Get variables, regular expressions FTW
     preg_match_all('#<\!-- VAR ([A-z0-9_-]*) -->(.*?)<\!-- ENDVAR \\1 -->#is', $text, $matches);
+    
+    // Initialize return values
     $tplvars = Array();
-    for($i=0;$i<sizeof($matches[1]);$i++)
+    
+    // Loop through each match, setting $tplvars[ $first_subpattern ] to $second_subpattern
+    for ( $i = 0; $i < sizeof($matches[1]); $i++ )
     {
-      $tplvars[$matches[1][$i]] = $matches[2][$i];
+      $tplvars[ $matches[1][$i] ] = $matches[2][$i];
     }
+    
+    // All done!
     return $tplvars;
   }
-  function compile_template($text) {
+  
+  /**
+   * Compiles a block of template code.
+   * @param string The text to process
+   * @return string
+   */
+  
+  function compile_tpl_code($text)
+  {
+    // A random seed used to salt tags
+    $seed = md5 ( microtime() . mt_rand() );
+    
+    // Strip out PHP sections
+    preg_match_all('/<\?php(.+?)\?>/is', $text, $php_matches);
+    
+    foreach ( $php_matches[0] as $i => $match )
+    {
+      // Substitute the PHP section with a random tag
+      $tag = "{PHP:$i:$seed}";
+      $text = str_replace_once($match, $tag, $text);
+    }
+    
+    // Escape slashes and single quotes in template code
+    $text = str_replace('\\', '\\\\', $text);
+    $text = str_replace('\'', '\\\'', $text);
+    
+    // Initialize the PHP compiled code
+    $text = 'ob_start(); echo \''.$text.'\'; $tpl_code = ob_get_contents(); ob_end_clean(); return $tpl_code;';
+    
+    ##
+    ## Main rules
+    ##
+    
+    //
+    // Conditionals
+    //
+    
+    // If-else-end
+    $text = preg_replace('/<!-- BEGIN ([A-z0-9_-]+?) -->(.*?)<!-- BEGINELSE \\1 -->(.*?)<!-- END \\1 -->/is', '\'; if ( $this->tpl_bool[\'\\1\'] ) { echo \'\\2\'; } else { echo \'\\3\'; } echo \'', $text);
+    
+    // If-end
+    $text = preg_replace('/<!-- BEGIN ([A-z0-9_-]+?) -->(.*?)<!-- END \\1 -->/is', '\'; if ( $this->tpl_bool[\'\\1\'] ) { echo \'\\2\'; } echo \'', $text);
+    
+    // If not-else-end
+    $text = preg_replace('/<!-- BEGINNOT ([A-z0-9_-]+?) -->(.*?)<!-- BEGINELSE \\1 -->(.*?)<!-- END \\1 -->/is', '\'; if ( !$this->tpl_bool[\'\\1\'] ) { echo \'\\2\'; } else { echo \'\\3\'; } echo \'', $text);
+    
+    // If not-end
+    $text = preg_replace('/<!-- BEGINNOT ([A-z0-9_-]+?) -->(.*?)<!-- END \\1 -->/is', '\'; if ( !$this->tpl_bool[\'\\1\'] ) { echo \'\\2\'; } echo \'', $text);
+    
+    // If set-else-end
+    $text = preg_replace('/<!-- IFSET ([A-z0-9_-]+?) -->(.*?)<!-- BEGINELSE \\1 -->(.*?)<!-- END \\1 -->/is', '\'; if ( isset($this->tpl_strings[\'\\1\']) ) { echo \'\\2\'; } else { echo \'\\3\'; } echo \'', $text);
+    
+    // If set-end
+    $text = preg_replace('/<!-- IFSET ([A-z0-9_-]+?) -->(.*?)<!-- END \\1 -->/is', '\'; if ( isset($this->tpl_strings[\'\\1\']) ) { echo \'\\2\'; } echo \'', $text);
+    
+    // If plugin loaded-else-end
+    $text = preg_replace('/<!-- IFPLUGIN ([A-z0-9_\.-]+?) -->(.*?)<!-- BEGINELSE \\1 -->(.*?)<!-- END \\1 -->/is', '\'; if ( getConfig(\'plugin_\\1\') == \'1\' ) { echo \'\\2\'; } else { echo \'\\3\'; } echo \'', $text);
+    
+    // If plugin loaded-end
+    $text = preg_replace('/<!-- IFPLUGIN ([A-z0-9_\.-]+?) -->(.*?)<!-- END \\1 -->/is', '\'; if ( getConfig(\'plugin_\\1\') == \'1\' ) { echo \'\\2\'; } echo \'', $text);
+    
+    //
+    // Data substitution/variables
+    //
+    
+    // System messages
+    $text = preg_replace('/<!-- SYSMSG ([A-z0-9\._-]+?) -->/is', '\' . $this->tplWikiFormat($pages->sysMsg(\'\\1\')) . \'', $text);
+    
+    // Template variables
+    $text = preg_replace('/\{([A-z0-9_-]+?)\}/is', '\' . $this->tpl_strings[\'\\1\'] . \'', $text);
+    
+    // Reinsert PHP
+    
+    foreach ( $php_matches[1] as $i => $match )
+    {
+      // Substitute the random tag with the "real" PHP code
+      $tag = "{PHP:$i:$seed}";
+      $text = str_replace_once($tag, "'; $match echo '", $text);
+    }
+    
+    return $text;  
+    
+  }
+  
+  /**
+   * Compiles the contents of a given template file, possibly using a cached copy, and returns the compiled code.
+   * @param string Filename of template (header.tpl)
+   * @return string
+   */
+  
+  function compile_template($filename)
+  {
     global $db, $session, $paths, $template, $plugins; // Common objects
-    if(!is_file(ENANO_ROOT . '/themes/'.$this->theme.'/'.$text)) die('Cannot find '.$text.' file for style, exiting');
-    $n = $text;
-    $tpl_filename = ENANO_ROOT . '/cache/' . $this->theme . '-' . str_replace('/', '-', $n) . '.php';
-    if(!is_file(ENANO_ROOT . '/themes/'.$this->theme.'/'.$text)) die('Cannot find '.$text.' file for style, exiting');
-    if(file_exists($tpl_filename) && getConfig('cache_thumbs')=='1')
+    
+    // Full path to template file
+    $tpl_file_fullpath = ENANO_ROOT . '/themes/' . $this->theme . '/' . $filename;
+    
+    // Make sure the file exists
+    if ( !is_file($tpl_file_fullpath) )
     {
-      include($tpl_filename);
-      $text = file_get_contents(ENANO_ROOT . '/themes/'.$this->theme.'/'.$text);
-      if(isset($md5) && $md5 == md5($text)) {
+      die_semicritical('Cannot find template file',
+                       '<p>The template parser was asked to load the file "' . htmlspecialchars($filename) . '", but that file couldn\'t be found in the directory for
+                           the current theme.</p>
+                        <p>Additional debugging information:<br />
+                           <b>Theme currently in use: </b>' . $this->theme . '<br />
+                           <b>Requested file: </b>' . $file . '
+                           </p>');
+    }
+    
+    // Check for cached copy
+    // This will make filenames in the pattern of theme-file.tpl.php
+    $cache_file = ENANO_ROOT . '/cache/' . $this->theme . '-' . str_replace('/', '-', $filename) . '.php';
+    
+    // Only use cached copy if caching is enabled
+    //   (it is enabled by default I think)
+    if ( file_exists($cache_file) && getConfig('cache_thumbs') == '1' )
+    {
+      // Cache files are auto-generated, but otherwise are normal PHP files
+      include($cache_file);
+      
+      // Fetch content of the ORIGINAL
+      $text = file_get_contents($tpl_file_fullpath);
+      
+      // $md5 will be set by the cached file
+      // This makes sure that a cached copy of the template is used only if its MD5
+      // matches the MD5 of the file that the compiled file was compiled from.
+      if ( isset($md5) && $md5 == md5($text) )
+      {
         return str_replace('\\"', '"', $tpl_text);
       }
     }
-    $text = file_get_contents(ENANO_ROOT . '/themes/'.$this->theme.'/'.$n);
     
+    // We won't use the cached copy here
+    $text = file_get_contents($tpl_file_fullpath);
+    
+    // This will be used later when writing the cached file
     $md5 = md5($text);
     
-    $seed = md5 ( microtime() . mt_rand() );
-    preg_match_all("/<\?php(.*?)\?>/is", $text, $m);
-    //die('<pre>'.htmlspecialchars(print_r($m, true)).'</pre>');
-    for($i = 0; $i < sizeof($m[1]); $i++)
+    // Preprocessing and checks complete - compile the code
+    $text = $this->compile_tpl_code($text);
+    
+    // Perhaps caching is enabled and the admin has changed the template?
+    if ( is_writable( ENANO_ROOT . '/cache/' ) && getConfig('cache_thumbs') == '1' )
     {
-      $text = str_replace("<?php{$m[1][$i]}?>", "{PHPCODE:{$i}:{$seed}}", $text);
-    }
-    //die('<pre>'.htmlspecialchars($text).'</pre>');
-    $text = 'ob_start(); echo \''.str_replace('\'', '\\\'', $text).'\'; $tpl_code = ob_get_contents(); ob_end_clean();';
-    $text = preg_replace('#<!-- BEGIN (.*?) -->#is', '\'; if(isset($this->tpl_bool[\'\\1\']) && $this->tpl_bool[\'\\1\']) { echo \'', $text);
-    $text = preg_replace('#<!-- IFSET (.*?) -->#is', '\'; if(isset($this->tpl_strings[\'\\1\'])) { echo \'', $text);
-    $text = preg_replace('#<!-- IFPLUGIN (.*?) -->#is', '\'; if(getConfig(\'plugin_\\1\')==\'1\') { echo \'', $text);
-    $text = preg_replace('#<!-- SYSMSG (.*?) -->#is', '\'; echo $template->tplWikiFormat($paths->sysMsg(\'\\1\')); echo \'', $text);
-    $text = preg_replace('#<!-- BEGINNOT (.*?) -->#is', '\'; if(!$this->tpl_bool[\'\\1\']) { echo \'', $text);
-    $text = preg_replace('#<!-- BEGINELSE (.*?) -->#is', '\'; } else { echo \'', $text);
-    $text = preg_replace('#<!-- END (.*?) -->#is', '\'; } echo \'', $text);
-    $text = preg_replace('#\{([A-z0-9]*)\}#is', '\'.$this->tpl_strings[\'\\1\'].\'', $text);
-    for($i = 0; $i < sizeof($m[1]); $i++)
-    {
-      $text = str_replace("{PHPCODE:{$i}:{$seed}}", "'; {$m[1][$i]} echo '", $text);
-    }
-    if(is_writable(ENANO_ROOT.'/cache/') && getConfig('cache_thumbs')=='1')
-    {
-      //die($tpl_filename);
-      $h = fopen($tpl_filename, 'w');
-      if(!$h) return $text;
-      $t = addslashes($text);
+      $h = fopen($cache_file, 'w');
+      if ( !$h )
+      {
+        // Couldn't open the file - silently ignore and return
+        return $text;
+      }
+      
+      // Escape the compiled code so it can be eval'ed
+      $text_escaped = addslashes($text);
       $notice = <<<EOF
 
 /*
@@ -951,37 +1110,34 @@
  */
 
 EOF;
-      fwrite($h, '<?php ' . $notice . ' $md5 = \''.$md5.'\'; $tpl_text = \''.$t.'\'; ?>');
+      // This is really just a normal PHP file that sets a variable or two and exits.
+      // $tpl_text actually will contain the compiled code
+      fwrite($h, '<?php ' . $notice . ' $md5 = \'' . $md5 . '\'; $tpl_text = \'' . $text_escaped . '\'; ?>');
       fclose($h);
     }
+    
     return $text; //('<pre>'.htmlspecialchars($text).'</pre>');
   }
   
-  function compile_template_text($text) {
-    $seed = md5 ( microtime() . mt_rand() );
-    preg_match_all("/<\?php(.*?)\?>/is", $text, $m);
-    //die('<pre>'.htmlspecialchars(print_r($m, true)).'</pre>');
-    for($i = 0; $i < sizeof($m[1]); $i++)
-    {
-      $text = str_replace("<?php{$m[1][$i]}?>", "{PHPCODE:{$i}:{$seed}}", $text);
-    }
-    //die('<pre>'.htmlspecialchars($text).'</pre>');
-    $text = 'ob_start(); echo \''.str_replace('\'', '\\\'', $text).'\'; $tpl_code = ob_get_contents(); ob_end_clean(); return $tpl_code;';
-    $text = preg_replace('#<!-- BEGIN (.*?) -->#is', '\'; if(isset($this->tpl_bool[\'\\1\']) && $this->tpl_bool[\'\\1\']) { echo \'', $text);
-    $text = preg_replace('#<!-- IFSET (.*?) -->#is', '\'; if(isset($this->tpl_strings[\'\\1\'])) { echo \'', $text);
-    $text = preg_replace('#<!-- IFPLUGIN (.*?) -->#is', '\'; if(getConfig(\'plugin_\\1\')==\'1\') { echo \'', $text);
-    $text = preg_replace('#<!-- SYSMSG (.*?) -->#is', '\'; echo $template->tplWikiFormat($paths->sysMsg(\'\\1\')); echo \'', $text);
-    $text = preg_replace('#<!-- BEGINNOT (.*?) -->#is', '\'; if(!$this->tpl_bool[\'\\1\']) { echo \'', $text);
-    $text = preg_replace('#<!-- BEGINELSE (.*?) -->#is', '\'; } else { echo \'', $text);
-    $text = preg_replace('#<!-- END (.*?) -->#is', '\'; } echo \'', $text);
-    $text = preg_replace('#\{([A-z0-9]*)\}#is', '\'.$this->tpl_strings[\'\\1\'].\'', $text);
-    for($i = 0; $i < sizeof($m[1]); $i++)
-    {
-      $text = str_replace("{PHPCODE:{$i}:{$seed}}", "'; {$m[1][$i]} echo '", $text);
-    }
-    return $text; //('<pre>'.htmlspecialchars($text).'</pre>');
+  
+  /**
+   * Compiles (parses) some template code with the current master set of variables and booleans.
+   * @param string Text to process
+   * @return string
+   */
+  
+  function compile_template_text($text)
+  {
+    // this might do something else in the future, possibly cache large templates
+    return $this->compile_tpl_code($text);
   }
   
+  /**
+   * For convenience - compiles AND parses some template code.
+   * @param string Text to process
+   * @return string
+   */
+  
   function parse($text)
   {
     $text = $this->compile_template_text($text);
@@ -1004,7 +1160,18 @@
   // So you can implement custom logic into your sidebar if you wish.
   // "Real" PHP support coming soon :-D
   
-  function tplWikiFormat($message, $filter_links = false, $filename = 'elements.tpl') {
+  /**
+   * Takes a blob of HTML with the specially formatted template-oriented wikitext and formats it. Does not use eval().
+   * This function butchers every coding standard in Enano and should eventually be rewritten. The fact is that the
+   * code _works_ and does a good job of checking for errors and cleanly complaining about them.
+   * @param string Text to process
+   * @param bool Ignored for backwards compatibility
+   * @param string File to get variables for sidebar data from
+   * @return string
+   */
+  
+  function tplWikiFormat($message, $filter_links = false, $filename = 'elements.tpl')
+  {
     global $db, $session, $paths, $template, $plugins; // Common objects
     $filter_links = false;
     $tplvars = $this->extract_vars($filename);
@@ -1029,83 +1196,93 @@
     
     // Conditionals
     
-    preg_match_all('#\{if ([A-Za-z0-9_ &\|\!-]*)\}(.*?)\{\/if\}#is', $message, $links);
+    preg_match_all('#\{if ([A-Za-z0-9_ \(\)&\|\!-]*)\}(.*?)\{\/if\}#is', $message, $links);
     
-    for($i=0;$i<sizeof($links[1]);$i++)
+    // Temporary exception from coding standards - using tab length of 4 here for clarity
+    for ( $i = 0; $i < sizeof($links[1]); $i++ )
     {
-      $message = str_replace('{if '.$links[1][$i].'}'.$links[2][$i].'{/if}', '{CONDITIONAL:'.$i.':'.$random_id.'}', $message);
-      
-      // Time for some manual parsing...
-      $chk = false;
-      $current_id = '';
-      $prn_level = 0;
-      // Used to keep track of where we are in the conditional
-      // Object of the game: turn {if this && ( that OR !something_else )} ... {/if} into if( ( isset($this->tpl_bool['that']) && $this->tpl_bool['that'] ) && ...
-      // Method of attack: escape all variables, ignore all else. Non-valid code is filtered out by a regex above.
-      $in_var_now = true;
-      $in_var_last = false;
-      $current_var = '';
-      $current_var_start_pos = 0;
-      $current_var_end_pos   = 0;
-      $j = -1;
-      $links[1][$i] = $links[1][$i] . ' ';
-      $d = strlen($links[1][$i]);
-      while($j < $d)
-      {
-        $j++;
-        $in_var_last = $in_var_now;
+        $condition =& $links[1][$i];
+        $message = str_replace('{if '.$condition.'}'.$links[2][$i].'{/if}', '{CONDITIONAL:'.$i.':'.$random_id.'}', $message);
         
-        $char = substr($links[1][$i], $j, 1);
-        $in_var_now = ( preg_match('#^([A-z0-9_]*){1}$#', $char) ) ? true : false;
-        if(!$in_var_last && $in_var_now)
-        {
-          $current_var_start_pos = $j;
-        }
-        if($in_var_last && !$in_var_now)
-        {
-          $current_var_end_pos = $j;
-        }
-        if($in_var_now)
+        // Time for some manual parsing...
+        $chk = false;
+        $current_id = '';
+        $prn_level = 0;
+        // Used to keep track of where we are in the conditional
+        // Object of the game: turn {if this && ( that OR !something_else )} ... {/if} into if( ( isset($this->tpl_bool['that']) && $this->tpl_bool['that'] ) && ...
+        // Method of attack: escape all variables, ignore all else. Non-valid code is filtered out by a regex above.
+        $in_var_now = true;
+        $in_var_last = false;
+        $current_var = '';
+        $current_var_start_pos = 0;
+        $current_var_end_pos     = 0;
+        $j = -1;
+        $condition = $condition . ' ';
+        $d = strlen($condition);
+        while($j < $d)
         {
-          $current_var .= $char;
-          continue;
+            $j++;
+            $in_var_last = $in_var_now;
+            
+            $char = substr($condition, $j, 1);
+            $in_var_now = ( preg_match('#^([A-z0-9_]*){1}$#', $char) ) ? true : false;
+            if(!$in_var_last && $in_var_now)
+            {
+                $current_var_start_pos = $j;
+            }
+            if($in_var_last && !$in_var_now)
+            {
+                $current_var_end_pos = $j;
+            }
+            if($in_var_now)
+            {
+                $current_var .= $char;
+                continue;
+            }
+            // OK we are not inside of a variable. That means that we JUST hit the end because the counter ($j) will be advanced to the beginning of the next variable once processing here is complete.
+            if($char != ' ' && $char != '(' && $char != ')' && $char != 'A' && $char != 'N' && $char != 'D' && $char != 'O' && $char != 'R' && $char != '&' && $char != '|' && $char != '!' && $char != '<' && $char != '>' && $char != '0' && $char != '1' && $char != '2' && $char != '3' && $char != '4' && $char != '5' && $char != '6' && $char != '7' && $char != '8' && $char != '9')
+            {
+                // XSS attack! Bail out
+                $errmsg    = '<p><b>Error:</b> Syntax error (possibly XSS attack) caught in template code:</p>';
+                $errmsg .= '<pre>';
+                $errmsg .= '{if '.htmlspecialchars($condition).'}';
+                $errmsg .= "\n    ";
+                for ( $k = 0; $k < $j; $k++ )
+                {
+                    $errmsg .= " ";
+                }
+                // Show position of error
+                $errmsg .= '<span style="color: red;">^</span>';
+                $errmsg .= '</pre>';
+                $message = str_replace('{CONDITIONAL:'.$i.':'.$random_id.'}', $errmsg, $message);
+                continue 2;
+            }
+            if($current_var != '')
+            {
+                $cd = '( isset($this->tpl_bool[\''.$current_var.'\']) && $this->tpl_bool[\''.$current_var.'\'] )';
+                $cvt = substr($condition, 0, $current_var_start_pos) . $cd . substr($condition, $current_var_end_pos, strlen($condition));
+                $j = $j + strlen($cd) - strlen($current_var);
+                $current_var = '';
+                $condition = $cvt;
+                $d = strlen($condition);
+            }
         }
-        // OK we are not inside of a variable. That means that we JUST hit the end because the counter ($j) will be advanced to the beginning of the next variable once processing here is complete.
-        if($char != ' ' && $char != '(' && $char != ')' && $char != 'A' && $char != 'N' && $char != 'D' && $char != 'O' && $char != 'R' && $char != '&' && $char != '|' && $char != '!' && $char != '<' && $char != '>' && $char != '0' && $char != '1' && $char != '2' && $char != '3' && $char != '4' && $char != '5' && $char != '6' && $char != '7' && $char != '8' && $char != '9')
-        {
-          // XSS attack! Bail out
-          echo '<p><b>Error:</b> Syntax error (possibly XSS attack) caught in template code:</p>';
-          echo '<pre>';
-          echo '{if '.$links[1][$i].'}';
-          echo "\n    ";
-          for($k=0;$k<$j;$k++) echo " ";
-          echo '<span style="color: red;">^</span>';
-          echo '</pre>';
-          continue 2;
-        }
-        if($current_var != '')
+        $condition = substr($condition, 0, strlen($condition)-1);
+        $condition = '$chk = ( '.$condition.' ) ? true : false;';
+        eval($condition);
+        
+        if($chk)
         {
-          $cd = '( isset($this->tpl_bool[\''.$current_var.'\']) && $this->tpl_bool[\''.$current_var.'\'] )';
-          $cvt = substr($links[1][$i], 0, $current_var_start_pos) . $cd . substr($links[1][$i], $current_var_end_pos, strlen($links[1][$i]));
-          $j = $j + strlen($cd) - strlen($current_var);
-          $current_var = '';
-          $links[1][$i] = $cvt;
-          $d = strlen($links[1][$i]);
+            if(strstr($links[2][$i], '{else}')) $c = substr($links[2][$i], 0, strpos($links[2][$i], '{else}'));
+            else $c = $links[2][$i];
+            $message = str_replace('{CONDITIONAL:'.$i.':'.$random_id.'}', $c, $message);
         }
-      }
-      $links[1][$i] = substr($links[1][$i], 0, strlen($links[1][$i])-1);
-      $links[1][$i] = '$chk = ( '.$links[1][$i].' ) ? true : false;';
-      eval($links[1][$i]);
-      
-      if($chk) { // isset($this->tpl_bool[$links[1][$i]]) && $this->tpl_bool[$links[1][$i]]
-        if(strstr($links[2][$i], '{else}')) $c = substr($links[2][$i], 0, strpos($links[2][$i], '{else}'));
-        else $c = $links[2][$i];
-        $message = str_replace('{CONDITIONAL:'.$i.':'.$random_id.'}', $c, $message);
-      } else {
-        if(strstr($links[2][$i], '{else}')) $c = substr($links[2][$i], strpos($links[2][$i], '{else}')+6, strlen($links[2][$i]));
-        else $c = '';
-        $message = str_replace('{CONDITIONAL:'.$i.':'.$random_id.'}', $c, $message);
-      }
+        else
+        {
+            if(strstr($links[2][$i], '{else}')) $c = substr($links[2][$i], strpos($links[2][$i], '{else}')+6, strlen($links[2][$i]));
+            else $c = '';
+            $message = str_replace('{CONDITIONAL:'.$i.':'.$random_id.'}', $c, $message);
+        }
     }
     
     preg_match_all('#\{!if ([A-Za-z_-]*)\}(.*?)\{\/if\}#is', $message, $links);
@@ -1174,26 +1351,26 @@
     // $message = preg_replace('#\[(http|ftp|irc):\/\/([a-z0-9\/:_\.\?&%\#@_\\\\-]+?) ([^\]]+)\\]#', '<a href="\\1://\\2">\\3</a><br style="display: none;" />', $message);
     // $message = preg_replace('#\[(http|ftp|irc):\/\/([a-z0-9\/:_\.\?&%\#@_\\\\-]+?)\\]#', '<a href="\\1://\\2">\\1://\\2</a><br style="display: none;" />', $message);
     
-    preg_match_all('#\[(http|ftp|irc):\/\/([a-z0-9\/:_\.\?&%\#@_\\\\-]+?)\\ ([^\]]+)]#', $message, $ext_link);
+    preg_match_all('/\[((https?|ftp|irc):\/\/([^@\]"\':]+)?((([a-z0-9-]+\.)*)[a-z0-9-]+)(\/[A-z0-9_%\|~`!\!@#\$\^&\*\(\):;\.,\/-]*(\?(([a-z0-9_-]+)(=[A-z0-9_%\|~`\!@#\$\^&\*\(\):;\.,\/-\[\]]+)?((&([a-z0-9_-]+)(=[A-z0-9_%\|~`!\!@#\$\^&\*\(\):;\.,\/-]+)?)*))?)?)?) ([^\]]+)\]/is', $message, $ext_link);
     
     for ( $i = 0; $i < count($ext_link[0]); $i++ )
     {
       $text_parser->assign_vars(Array(  
-          'HREF'  => "{$ext_link[1][$i]}://{$ext_link[2][$i]}",
+          'HREF'  => $ext_link[1][$i],
           'FLAGS' => '',
-          'TEXT'  => $ext_link[3][$i]
+          'TEXT'  => $ext_link[16][$i]
         ));
       $message = str_replace($ext_link[0][$i], $text_parser->run(), $message);
     }
     
-    preg_match_all('#\[(http|ftp|irc):\/\/([a-z0-9\/:_\.\?&%\#@_\\\\-]+?)\\]#', $message, $ext_link);
+    preg_match_all('/\[((https?|ftp|irc):\/\/([^@\]"\':]+)?((([a-z0-9-]+\.)*)[a-z0-9-]+)(\/[A-z0-9_%\|~`!\!@#\$\^&\*\(\):;\.,\/-]*(\?(([a-z0-9_-]+)(=[A-z0-9_%\|~`\!@#\$\^&\*\(\):;\.,\/-\[\]]+)?((&([a-z0-9_-]+)(=[A-z0-9_%\|~`!\!@#\$\^&\*\(\):;\.,\/-]+)?)*))?)?)?)\]/is', $message, $ext_link);
     
     for ( $i = 0; $i < count($ext_link[0]); $i++ )
     {
       $text_parser->assign_vars(Array(  
-          'HREF'  => "{$ext_link[1][$i]}://{$ext_link[2][$i]}",
+          'HREF'  => $ext_link[1][$i],
           'FLAGS' => '',
-          'TEXT'  => htmlspecialchars("{$ext_link[1][$i]}://{$ext_link[2][$i]}")
+          'TEXT'  => htmlspecialchars($ext_link[1][$i])
         ));
       $message = str_replace($ext_link[0][$i], $text_parser->run(), $message);
     }
--- a/includes/wikiengine/Tables.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/includes/wikiengine/Tables.php	Sun Oct 07 21:31:14 2007 -0400
@@ -1,8 +1,8 @@
 <?php
 
-/**
+/*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  *
  * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
@@ -422,6 +422,7 @@
 	 * @return array
 	 */
 	function setupAttributeWhitelist() {
+    global $db, $session, $paths, $template, $plugins;
 		$common = array( 'id', 'class', 'lang', 'dir', 'title', 'style' );
 		$block = array_merge( $common, array( 'align' ) );
 		$tablealign = array( 'align', 'char', 'charoff', 'valign' );
@@ -570,6 +571,14 @@
       # XHTML stuff
       'acronym'    => $common
 			);
+    
+    // custom tags can be added by plugins
+    $code = $plugins->setHook('html_attribute_whitelist');
+    foreach ( $code as $cmd )
+    {
+      eval($cmd);
+    }
+    
 		return $whitelist;
 	}
   
--- a/index.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/index.php	Sun Oct 07 21:31:14 2007 -0400
@@ -1,8 +1,8 @@
 <?php
 
-/**
+/*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * @Version 1.0.2 (Coblynau)
+ * Version 1.0.2 (Coblynau)
  * Copyright (C) 2006-2007 Dan Fuhry
  *
  * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
@@ -15,7 +15,7 @@
 
   // Set up gzip encoding before any output is sent
   
-  $aggressive_optimize_html = true;
+  $aggressive_optimize_html = false;
   
   global $do_gzip;
   $do_gzip = true;
@@ -141,6 +141,11 @@
           <input type="submit" name="_cancel" value="Cancel" />
         </form>
       ';
+      if ( getConfig('wiki_edit_notice') == '1' )
+      {
+        $notice = getConfig('wiki_edit_notice_text');
+        echo RenderMan::render($notice);
+      }
       $template->footer();
       break;
     case 'viewsource':
--- a/install.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/install.php	Sun Oct 07 21:31:14 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  * install.php - handles everything related to installation and initial configuration
  *
@@ -15,7 +15,7 @@
  
 @include('config.php');
 if( ( defined('ENANO_INSTALLED') || defined('MIDGET_INSTALLED') ) && ((isset($_GET['mode']) && ($_GET['mode']!='finish' && $_GET['mode']!='css')) || !isset($_GET['mode']))) {
-  $_GET['title'] = 'Enano:WhoCaresWhatThisIs';
+  $_GET['title'] = 'Enano:Installation_locked';
   require('includes/common.php');
   die_friendly('Installation locked', '<p>The Enano installer has found a Enano installation in this directory. You MUST delete config.php if you want to re-install Enano.</p><p>If you wish to upgrade an older Enano installation to this version, please use the <a href="upgrade.php">upgrade script</a>.</p>');
   exit;
@@ -23,9 +23,8 @@
 
 define('IN_ENANO_INSTALL', 'true');
 
-define('ENANO_VERSION', '1.0.2');
+define('ENANO_VERSION', '1.1.1');
 // In beta versions, define ENANO_BETA_VERSION here
-define('ENANO_BETA_VERSION', '1');
 
 if(!defined('scriptPath')) {
   $sp = dirname($_SERVER['REQUEST_URI']);
@@ -316,8 +315,7 @@
     <div style="text-align: center; margin-top: 10px;">
       <img alt="[ Enano CMS Project logo ]" src="images/enano-artwork/installer-greeting-green.png" style="display: block; margin: 0 auto; padding-left: 100px;" />
       <h2>Welcome to Enano</h2>
-      <h3>version 1.0.2 &ndash; beta 1<br />
-      <span style="font-weight: normal;">also affectionately known as "coblynau" <tt>:)</tt></span></h3>
+      <h3>version 1.1.1 &ndash; unstable</h3>
       <?php
       if ( file_exists('./_nightly.php') )
       {
@@ -1066,7 +1064,8 @@
       $schema = str_replace('{{TABLE_PREFIX}}', $_POST['table_prefix'],                          $schema);
       $schema = str_replace('{{VERSION}}',      ENANO_VERSION,                                   $schema);
       $schema = str_replace('{{ADMIN_EMBED_PHP}}', $_POST['admin_embed_php'],                    $schema);
-      $schema = str_replace('{{BETA_VERSION}}', ENANO_BETA_VERSION,                              $schema);
+      // Not anymore!! :-D
+      // $schema = str_replace('{{BETA_VERSION}}', ENANO_BETA_VERSION,                              $schema);
       
       if(isset($_POST['wiki_mode']))
       {
--- a/licenses/index.html	Sun Sep 30 19:22:04 2007 -0400
+++ b/licenses/index.html	Sun Oct 07 21:31:14 2007 -0400
@@ -110,6 +110,7 @@
 <p><a href="bsdlic.html">View the text of this license</a></p>
 <ul>
   <li><a href="http://pajhome.org.uk/">Paul Johnston</a>'s implementations of the MD5 and SHA1 algorithms in Javascript</li>
+  <li><a href="http://labs.adobe.com/technologies/spry/">Adobe Spry</a>, used for some Javascript effects</li>
 </ul>
 
 <h2>The MIT/X License</h2>
--- a/plugins/SpecialAdmin.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/plugins/SpecialAdmin.php	Sun Oct 07 21:31:14 2007 -0400
@@ -10,7 +10,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  *
  * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
@@ -203,6 +203,16 @@
       setConfig('pw_strength_minimum', $strength);
     }
     
+    // Account lockout policy
+    if ( preg_match('/^[0-9]+$/', $_POST['lockout_threshold']) )
+      setConfig('lockout_threshold', $_POST['lockout_threshold']);
+    
+    if ( preg_match('/^[0-9]+$/', $_POST['lockout_duration']) )
+      setConfig('lockout_duration', $_POST['lockout_duration']);
+    
+    if ( in_array($_POST['lockout_policy'], array('disable', 'captcha', 'lockout')) )
+      setConfig('lockout_policy', $_POST['lockout_policy']);
+    
     echo '<div class="info-box">Your changes to the site configuration have been saved.</div><br />';
     
   }
@@ -351,6 +361,43 @@
         </td>
       </tr>
       
+    <!-- Account lockout -->
+    
+      <tr><th colspan="2">Account lockouts</th></tr>
+      
+      <tr><td class="row3" colspan="2">Configure Enano to prevent or restrict logins for a specified period of time if a user enters an incorrect password a specific number of times.</td></tr>
+      
+      <tr>
+        <td class="row2">Lockout threshold:<br />
+          <small>How many times can a user enter wrong credentials before a lockout goes into effect?</small>
+        </td>
+        <td class="row2">
+          <input type="text" name="lockout_threshold" value="<?php echo ( $_ = getConfig('lockout_threshold') ) ? $_ : '5' ?>" />
+        </td>
+      </tr>
+      
+      <tr>
+        <td class="row1">Lockout duration:<br />
+          <small>This is how long an account lockout should last, in minutes.</small>
+        </td>
+        <td class="row1">
+          <input type="text" name="lockout_duration" value="<?php echo ( $_ = getConfig('lockout_duration') ) ? $_ : '15' ?>" />
+        </td>
+      </tr>
+      
+      <tr>
+        <td class="row2">Lockout policy:<br />
+          <small>What should be done when a lockout goes into effect?</small>
+        </td>
+        <td class="row2">
+          <label><input type="radio" name="lockout_policy" value="disable" <?php if ( getConfig('lockout_policy') == 'disable' ) echo 'checked="checked"'; ?> /> Don't do anything</label><br />
+          <label><input type="radio" name="lockout_policy" value="captcha" <?php if ( getConfig('lockout_policy') == 'captcha' ) echo 'checked="checked"'; ?> /> Require visual confirmation</label><br />
+          <label><input type="radio" name="lockout_policy" value="lockout" <?php if ( getConfig('lockout_policy') == 'lockout' || !getConfig('lockout_policy') ) echo 'checked="checked"'; ?> /> Prevent all login attempts</label>
+        </td>
+      </tr>
+      
+    <!-- Password strength -->
+      
       <tr><th colspan="2">Password strength</th></tr>
       
       <tr>
@@ -2685,7 +2732,7 @@
           } 
           else 
           {
-            echo '<div class="wait-box">Please wait while the administration panel loads. You need to be using a recent browser with AJAX support in order to use Runt.</div>';
+            echo '<script type="text/javascript">document.write(\'<div class="wait-box">Please wait while the administration panel loads. You need to be using a recent browser with AJAX support in order to use Runt.</div>\');</script><noscript><div class="error-box">It looks like Javascript isn\'t enabled in your browser. Please enable Javascript or use a different browser to continue.</div></noscript>';
           }
           ?>
           </div>
@@ -2834,9 +2881,8 @@
     
     if(isset($_GET['action']) && isset($_GET['id']))
     {
-      if(preg_match('#^([0-9]*)$#', $_GET['id']))
+      if(!preg_match('#^([0-9]*)$#', $_GET['id']))
       {
-      } else {
         echo '<div class="warning-box">Error with action: $_GET["id"] was not an integer, aborting to prevent SQL injection</div>';
       }
       switch($_GET['action'])
--- a/plugins/SpecialPageFuncs.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/plugins/SpecialPageFuncs.php	Sun Oct 07 21:31:14 2007 -0400
@@ -151,13 +151,13 @@
     {
       $db->_die('The page entry could not be inserted.');
     }
-    $q = $db->sql_query('INSERT INTO '.table_prefix.'page_text(page_id,namespace,page_text) VALUES(\''.$urlname.'\', \''.$_POST['namespace'].'\', \''.$db->escape('Please edit this page! <nowiki><script type="text/javascript">ajaxEditor();</script></nowiki>').'\');');
+    $q = $db->sql_query('INSERT INTO '.table_prefix.'page_text(page_id,namespace,page_text) VALUES(\''.$urlname.'\', \''.$_POST['namespace'].'\', \''.'\');');
     if ( !$q )
     {
       $db->_die('The page text entry could not be inserted.');
     }
     
-    header('Location: '.makeUrlNS($_POST['namespace'], sanitize_page_id($p)));
+    header('Location: '.makeUrlNS($_POST['namespace'], sanitize_page_id($p)) . '#do:edit');
     exit;
   }
   $template->header();
--- a/plugins/SpecialSearch.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/plugins/SpecialSearch.php	Sun Oct 07 21:31:14 2007 -0400
@@ -100,18 +100,21 @@
   if(!empty($q))
   {
     // See if any pages directly match the title
-          
-    for ( $i = 0; $i < count ( $paths->pages ) / 2; $i++ )
+    
+    if ( strlen($q) >= 4 )
     {
-      $pg =& $paths->pages[$i];
-      $q_lc = strtolower( str_replace(' ', '_', $q) );
-      $q_tl = strtolower( str_replace('_', ' ', $q) );
-      $p_lc = strtolower($pg['urlname']);
-      $p_tl = strtolower($pg['name']);
-      if ( strstr($p_tl, $q_tl) || strstr($p_lc, $q_lc) && $pg['visible'] == 1 )
+      for ( $i = 0; $i < count ( $paths->pages ) / 2; $i++ )
       {
-        echo '<div class="usermessage">Perhaps you were looking for <b><a href="' . makeUrl($pg['urlname'], false, true) . '">' . htmlspecialchars($pg['name']) . '</a></b>?</div>';
-        break;
+        $pg =& $paths->pages[$i];
+        $q_lc = strtolower( str_replace(' ', '_', $q) );
+        $q_tl = strtolower( str_replace('_', ' ', $q) );
+        $p_lc = strtolower($pg['urlname']);
+        $p_tl = strtolower($pg['name']);
+        if ( strstr($p_tl, $q_tl) || strstr($p_lc, $q_lc) && $pg['visible'] == 1 )
+        {
+          echo '<div class="usermessage">Perhaps you were looking for <b><a href="' . makeUrl($pg['urlname'], false, true) . '">' . htmlspecialchars($pg['name']) . '</a></b>?</div>';
+          break;
+        }
       }
     }
           
--- a/plugins/SpecialUserFuncs.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/plugins/SpecialUserFuncs.php	Sun Oct 07 21:31:14 2007 -0400
@@ -104,14 +104,60 @@
   $pubkey = $session->rijndael_genkey();
   $challenge = $session->dss_rand();
   
+  $locked_out = false;
+  // are we locked out?
+  $threshold = ( $_ = getConfig('lockout_threshold') ) ? intval($_) : 5;
+  $duration  = ( $_ = getConfig('lockout_duration') ) ? intval($_) : 15;
+  // convert to minutes
+  $duration  = $duration * 60;
+  $policy = ( $x = getConfig('lockout_policy') && in_array(getConfig('lockout_policy'), array('lockout', 'disable', 'captcha')) ) ? getConfig('lockout_policy') : 'lockout';
+  if ( $policy != 'disable' )
+  {
+    $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']);
+    $timestamp_cutoff = time() - $duration;
+    $q = $session->sql('SELECT timestamp FROM '.table_prefix.'lockout WHERE timestamp > ' . $timestamp_cutoff . ' AND ipaddr = \'' . $ipaddr . '\' ORDER BY timestamp DESC;');
+    $fails = $db->numrows();
+    if ( $fails >= $threshold )
+    {
+      $row = $db->fetchrow();
+      $locked_out = true;
+      $lockdata = array(
+          'locked_out' => true,
+          'lockout_threshold' => $threshold,
+          'lockout_duration' => ( $duration / 60 ),
+          'lockout_fails' => $fails,
+          'lockout_policy' => $policy,
+          'lockout_last_time' => $row['timestamp'],
+          'server_time' => time(),
+          'captcha' => ''
+        );
+      if ( $policy == 'captcha' )
+      {
+        $lockdata['captcha'] = $session->make_captcha();
+      }
+    }
+    $db->free_result();
+  }
+  
   if ( isset($_GET['act']) && $_GET['act'] == 'getkey' )
   {
     $username = ( $session->user_logged_in ) ? $session->username : false;
     $response = Array(
       'username' => $username,
       'key' => $pubkey,
-      'challenge' => $challenge
+      'challenge' => $challenge,
+      'locked_out' => false
       );
+    
+    if ( $locked_out )
+    {
+      foreach ( $lockdata as $x => $y )
+      {
+        $response[$x] = $y;
+      }
+      unset($x, $y);
+    }
+    
     $json = new Services_JSON(SERVICES_JSON_LOOSE_TYPE);
     $response = $json->encode($response);
     echo $response;
@@ -138,7 +184,46 @@
   $header = ( $level > USER_LEVEL_MEMBER ) ? 'Please re-enter your login details' : 'Please enter your username and password to log in.';
   if ( isset($_POST['login']) )
   {
-    echo '<p>'.$__login_status.'</p>';
+    $errstring = $__login_status['error'];
+    switch($__login_status['error'])
+    {
+      case 'key_not_found':
+        $errstring = 'Enano couldn\'t look up the encryption key used to encrypt your password. This most often happens if a cache rotation occurred during your login attempt, or if you refreshed the login page.';
+        break;
+      case 'key_wrong_length':
+        $errstring = 'The encryption key was the wrong length.';
+        break;
+      case 'too_big_for_britches':
+        $errstring = 'You are trying to authenticate at a level that your user account does not permit.';
+        break;
+      case 'invalid_credentials':
+        $errstring = 'You have entered an invalid username or password. Please enter your login details again.';
+        if ( $__login_status['lockout_policy'] == 'lockout' )
+        {
+          $errstring .= ' You have used up '.$__login_status['lockout_fails'].' out of '.$__login_status['lockout_threshold'].' login attempts. After you have used up all '.$data['lockout_threshold'].' login attempts, you will be locked out from logging in for '.$__login_status['lockout_duration'].' minutes.';
+        }
+        else if ( $__login_status['lockout_policy'] == 'captcha' )
+        {
+          $errstring .= ' You have used up '.$__login_status['lockout_fails'].' out of '.$__login_status['lockout_threshold'].' login attempts. After you have used up all '.$data['lockout_threshold'].' login attempts, you will have to enter a visual confirmation code before logging in, effective for '.$__login_status['lockout_duration'].' minutes.';
+        }
+        break;
+      case 'backend_fail':
+        $errstring = 'You entered the right credentials and everything was validated, but for some reason Enano couldn\'t register your session. This is an internal problem with the site and you are encouraged to contact site administration.';
+        break;
+      case 'locked_out':
+        $attempts = intval($__login_status['lockout_fails']);
+        if ( $attempts > $__login_status['lockout_threshold'])
+          $attempts = $__login_status['lockout_threshold'];
+        $time_rem = ( $__login_status['lockout_last_time'] % ( $__login_status['lockout_duration'] * 60 ) );
+        $time_rem = $__login_status['lockout_duration'] - round($time_rem / 60);
+        $s = ( $time_rem == 1 ) ? '' : 's';
+        $errstring = "You have used up all {$__login_status['lockout_threshold']} allowed login attempts. Please wait {$time_rem} minute$s before attempting to log in again";
+        if ( $__login_status['lockout_policy'] == 'captcha' )
+        $errstring .= ', or enter the visual confirmation code shown above in the appropriate box';
+        $errstring .= '.';
+        break;
+    }
+    echo '<div class="error-box-mini">'.$errstring.'</div>';
   }
   if ( $p = $paths->getAllParams() )
   {
@@ -189,7 +274,7 @@
               ?> />
           </td>
           <?php if ( $level <= USER_LEVEL_MEMBER ) { ?>
-          <td rowspan="2" class="row3">
+          <td rowspan="<?php echo ( ( $locked_out && $lockdata['lockout_policy'] == 'captcha' ) ) ? '4' : '2'; ?>" class="row3">
             <small>Forgot your password? <a href="<?php echo makeUrlNS('Special', 'PasswordReset'); ?>">No problem.</a><br />
             Maybe you need to <a href="<?php echo makeUrlNS('Special', 'Register'); ?>">create an account</a>.</small>
           </td>
@@ -198,6 +283,21 @@
         <tr>
           <td class="row2">Password:<br /></td><td class="row1"><input name="pass" size="25" type="password" tabindex="<?php echo ( $level <= USER_LEVEL_MEMBER ) ? '2' : '1'; ?>" /></td>
          </tr>
+         <?php
+         if ( $locked_out && $lockdata['lockout_policy'] == 'captcha' )
+         {
+           ?>
+           <tr>
+             <td class="row2" rowspan="2">Code in image:<br /></td><td class="row1"><input type="hidden" name="captcha_hash" value="<?php echo $lockdata['captcha']; ?>" /><input name="captcha_code" size="25" type="text" tabindex="<?php echo ( $level <= USER_LEVEL_MEMBER ) ? '3' : '4'; ?>" /></td>
+           </tr>
+           <tr>
+             <td class="row3">
+               <img src="<?php echo makeUrlNS('Special', 'Captcha/' . $lockdata['captcha']) ?>" onclick="this.src=this.src+'/a';" style="cursor: pointer;" />
+             </td>
+           </tr>
+           <?php
+         }
+         ?>
          <?php if ( $level <= USER_LEVEL_MEMBER ) { ?>
          <tr>
            <td class="row3" colspan="3">
@@ -242,12 +342,12 @@
     $plugins->attachHook('login_password_reset', 'SpecialLogin_SendResponse_PasswordReset($row[\'user_id\'], $row[\'temp_password\']);');
     $json = new Services_JSON(SERVICES_JSON_LOOSE_TYPE);
     $data = $json->decode($_POST['params']);
+    $captcha_hash = ( isset($data['captcha_hash']) ) ? $data['captcha_hash'] : false;
+    $captcha_code = ( isset($data['captcha_code']) ) ? $data['captcha_code'] : false;
     $level = ( isset($data['level']) ) ? intval($data['level']) : USER_LEVEL_MEMBER;
-    $result = $session->login_with_crypto($data['username'], $data['crypt_data'], $data['crypt_key'], $data['challenge'], $level);
+    $result = $session->login_with_crypto($data['username'], $data['crypt_data'], $data['crypt_key'], $data['challenge'], $level, $captcha_hash, $captcha_code);
     $session->start();
-    //echo "$result\n$session->sid_super";
-    //exit;
-    if ( $result == 'success' )
+    if ( $result['success'] )
     {
       $response = Array(
           'result' => 'success',
@@ -256,9 +356,16 @@
     }
     else
     {
+      $captcha = '';
+      if ( $result['error'] == 'locked_out' && $result['lockout_policy'] == 'captcha' )
+      {
+        $session->kill_captcha();
+        $captcha = $session->make_captcha();
+      }
       $response = Array(
           'result' => 'error',
-          'error' => $result
+          'data' => $result,
+          'captcha' => $captcha
         );
     }
     $response = $json->encode($response);
@@ -267,17 +374,19 @@
     exit;
   }
   if(isset($_POST['login'])) {
+    $captcha_hash = ( isset($_POST['captcha_hash']) ) ? $_POST['captcha_hash'] : false;
+    $captcha_code = ( isset($_POST['captcha_code']) ) ? $_POST['captcha_code'] : false;
     if($_POST['use_crypt'] == 'yes')
     {
-      $result = $session->login_with_crypto($_POST['username'], $_POST['crypt_data'], $_POST['crypt_key'], $_POST['challenge_data'], intval($_POST['auth_level']));
+      $result = $session->login_with_crypto($_POST['username'], $_POST['crypt_data'], $_POST['crypt_key'], $_POST['challenge_data'], intval($_POST['auth_level']), $captcha_hash, $captcha_code);
     }
     else
     {
-      $result = $session->login_without_crypto($_POST['username'], $_POST['pass'], false, intval($_POST['auth_level']));
+      $result = $session->login_without_crypto($_POST['username'], $_POST['pass'], false, intval($_POST['auth_level']), $captcha_hash, $captcha_code);
     }
     $session->start();
     $paths->init();
-    if($result == 'success')
+    if($result['success'])
     {
       $template->load_theme($session->theme, $session->style);
       if(isset($_POST['return_to']))
--- a/plugins/SpecialUserPrefs.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/plugins/SpecialUserPrefs.php	Sun Oct 07 21:31:14 2007 -0400
@@ -100,7 +100,7 @@
   global $db, $session, $paths, $template, $plugins; // Common objects
   global $userprefs_menu_links;
   
-  userprefs_menu_add('Profile/membership', 'Edit e-mail address and password', makeUrlNS('Special', 'Preferences/EmailPassword'));
+  userprefs_menu_add('Profile/membership', 'Edit e-mail address and password', makeUrlNS('Special', 'Preferences/EmailPassword') . '" onclick="ajaxLoginNavTo(\'Special\', \'Preferences/EmailPassword\', '.USER_LEVEL_CHPREF.'); return false;');
   userprefs_menu_add('Profile/membership', 'Edit signature', makeUrlNS('Special', 'Preferences/Signature'));
   userprefs_menu_add('Profile/membership', 'Edit public profile', makeUrlNS('Special', 'Preferences/Profile'));
   userprefs_menu_add('Private messages', 'Inbox', makeUrlNS('Special', 'PrivateMessages/Folder/Inbox'));
@@ -302,11 +302,14 @@
   {
     case 'Home':
       global $email;
-      $user_page = '<a href="' . makeUrlNS('User', str_replace(' ', '_', $session->username)) . '">user page</a> <sup>(<a href="' . makeUrlNS('User', str_replace(' ', '_', $session->username)) . '#do:comments">comments</a>)</sup>';
+      $userpage_id = $paths->nslist['User'] . sanitize_page_id($session->username);
+      $userpage_exists = ( isPage($userpage_id) ) ? '' : ' class="wikilink-nonexistent"';
+      $user_page = '<a href="' . makeUrlNS('User', sanitize_page_id($session->username)) . '"' . $userpage_exists . '>user page</a> <sup>(<a href="' . makeUrlNS('User', str_replace(' ', '_', $session->username)) . '#do:comments">comments</a>)</sup>';
       $site_admin = $email->encryptEmail(getConfig('contact_email'), '', '', 'administrator');
+      $make_one_now = '<a href="' . makeUrlNS('User', sanitize_page_id($session->username)) . '">make one now</a>';
       echo "<h3 style='margin-top: 0;'>$session->username, welcome to your control panel</h3>";
       echo "<p>Here you can make changes to your profile, view statistics on yourself on this site, and set your preferences.</p>
-            <p>If you have not already done so, you are encouraged to make a $user_page and tell the other members of this site a little about yourself.</p>
+            <p>Your $user_page is your free writing space. You can use it to tell the other members of this site a little bit about yourself. If you haven't already made a user page, why not $make_one_now?</p>
             <p>Use the menu at the top to navigate around. If you have any questions, you may contact the $site_admin.";
       break;
     case 'EmailPassword':
--- a/plugins/admin/PageGroups.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/plugins/admin/PageGroups.php	Sun Oct 07 21:31:14 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  *
  * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
@@ -48,7 +48,12 @@
             echo '<div class="error-box">Please specify at least one page to place in this group.</div>';
             return;
           }
-          if ( $_POST['group_type'] != PAGE_GRP_TAGGED && $_POST['group_type'] != PAGE_GRP_CATLINK && $_POST['group_type'] != PAGE_GRP_NORMAL )
+          if ( $_POST['group_type'] == PAGE_GRP_REGEX && empty($_POST['regex']) )
+          {
+            echo '<div class="error-box">Please specify a regular expression to match page IDs against.</div>';
+            return;
+          }
+          if ( $_POST['group_type'] != PAGE_GRP_TAGGED && $_POST['group_type'] != PAGE_GRP_CATLINK && $_POST['group_type'] != PAGE_GRP_NORMAL && $_POST['group_type'] != PAGE_GRP_REGEX )
           {
             echo '<div class="error-box">Umm, you sent an invalid group type. I\'d put a real error message here but this will only be shown if you try to hack the system.</div>';
             return;
@@ -103,6 +108,14 @@
               if ( !$q )
                 $db->_die();
               break;
+            case PAGE_GRP_REGEX:
+              $name  = $db->escape($_POST['pg_name']);
+              $regex = $db->escape($_POST['regex']);
+              $sql = 'INSERT INTO '.table_prefix.'page_groups(pg_type,pg_name,pg_target) VALUES(' . PAGE_GRP_REGEX . ', \'' . $name . '\', \'' . $regex . '\');';
+              $q = $db->sql_query($sql);
+              if ( !$q )
+                $db->_die();
+              break;
           }
           echo '<div class="info-box">The page group "' . htmlspecialchars($_POST['pg_name']) . '" has been created.</div>';
           break;
@@ -115,6 +128,7 @@
           var pg_normal  = <?php echo PAGE_GRP_NORMAL; ?>;
           var pg_tagged  = <?php echo PAGE_GRP_TAGGED; ?>;
           var pg_catlink = <?php echo PAGE_GRP_CATLINK; ?>;
+          var pg_regex   = <?php echo PAGE_GRP_REGEX; ?>;
           var selection = false;
           // Get selection
           for ( var i = 0; i < selector.childNodes.length; i++ )
@@ -135,7 +149,7 @@
             return true;
           }
           selection = parseInt(selection);
-          if ( selection != pg_normal && selection != pg_tagged && selection != pg_catlink )
+          if ( selection != pg_normal && selection != pg_tagged && selection != pg_catlink && selection != pg_regex )
           {
             alert('Invalid field value');
             return true;
@@ -156,6 +170,10 @@
             document.getElementById('pg_create_title_normal').style.display = 'inline';
             document.getElementById('pg_create_normal_1').style.display = 'block';
             document.getElementById('pg_create_normal_2').style.display = 'block';
+            
+            document.getElementById('pg_create_title_regex').style.display = 'none';
+            document.getElementById('pg_create_regex_1').style.display = 'none';
+            document.getElementById('pg_create_regex_2').style.display = 'none';
           }
           else if ( selection == pg_catlink )
           {
@@ -170,6 +188,10 @@
             document.getElementById('pg_create_title_normal').style.display = 'none';
             document.getElementById('pg_create_normal_1').style.display = 'none';
             document.getElementById('pg_create_normal_2').style.display = 'none';
+            
+            document.getElementById('pg_create_title_regex').style.display = 'none';
+            document.getElementById('pg_create_regex_1').style.display = 'none';
+            document.getElementById('pg_create_regex_2').style.display = 'none';
           }
           else if ( selection == pg_tagged )
           {
@@ -184,6 +206,28 @@
             document.getElementById('pg_create_title_normal').style.display = 'none';
             document.getElementById('pg_create_normal_1').style.display = 'none';
             document.getElementById('pg_create_normal_2').style.display = 'none';
+            
+            document.getElementById('pg_create_title_regex').style.display = 'none';
+            document.getElementById('pg_create_regex_1').style.display = 'none';
+            document.getElementById('pg_create_regex_2').style.display = 'none';
+          }
+          else if ( selection == pg_regex )
+          {
+            document.getElementById('pg_create_title_catlink').style.display = 'none';
+            document.getElementById('pg_create_catlink_1').style.display = 'none';
+            document.getElementById('pg_create_catlink_2').style.display = 'none';
+            
+            document.getElementById('pg_create_title_tagged').style.display = 'none';
+            document.getElementById('pg_create_tagged_1').style.display = 'none';
+            document.getElementById('pg_create_tagged_2').style.display = 'none';
+            
+            document.getElementById('pg_create_title_normal').style.display = 'none';
+            document.getElementById('pg_create_normal_1').style.display = 'none';
+            document.getElementById('pg_create_normal_2').style.display = 'none';
+            
+            document.getElementById('pg_create_title_regex').style.display = 'inline';
+            document.getElementById('pg_create_regex_1').style.display = 'block';
+            document.getElementById('pg_create_regex_2').style.display = 'block';
           }
         
         }
@@ -199,6 +243,10 @@
           document.getElementById('pg_create_tagged_1').style.display = 'none';
           document.getElementById('pg_create_tagged_2').style.display = 'none';
           
+          document.getElementById('pg_create_title_regex').style.display = 'none';
+          document.getElementById('pg_create_regex_1').style.display = 'none';
+          document.getElementById('pg_create_regex_2').style.display = 'none';
+          
           document.getElementById('pg_create_title_normal').style.display = 'inline';
           document.getElementById('pg_create_normal_1').style.display = 'block';
           document.getElementById('pg_create_normal_2').style.display = 'block';
@@ -292,6 +340,7 @@
                 <option value="' . PAGE_GRP_NORMAL  . '" selected="selected">Static group of pages</option>
                 <option value="' . PAGE_GRP_TAGGED  . '">Group of pages with one tag</option>
                 <option value="' . PAGE_GRP_CATLINK . '">Link to category</option>
+                <option value="' . PAGE_GRP_REGEX   . '">Perl-compatible regular expression (advanced)</option>
               </select>
               </td>
             </tr>';
@@ -308,6 +357,9 @@
                 <span id="pg_create_title_catlink">
                   Mirror a category
                 </span>
+                <span id="pg_create_title_regex">
+                  Filter through a regular expression
+                </span>
               </th>
             </tr>';
       
@@ -324,6 +376,14 @@
                 <div id="pg_create_tagged_1">
                   Include pages with this tag:
                 </div>
+                <div id="pg_create_regex_1">
+                  Regular expression:<br />
+                  <small>Be sure to include the starting and ending delimiters and any flags you might need.<br />
+                         These pages might help: <a href="http://us.php.net/manual/en/reference.pcre.pattern.modifiers.php">Pattern modifiers</a> &bull;
+                         <a href="http://us.php.net/manual/en/reference.pcre.pattern.syntax.php">Pattern syntax</a><br />
+                         Examples: <tt>/^(Special|Admin):/i</tt> &bull; <tt>/^Image:([0-9]+)$/</tt><br />
+                         Developers, remember that this will be matched against the full page identifier string. This means that <tt>/^About_Enano$/</tt>
+                         will NOT match the page Special:About_Enano.</small>
               </td>';
             
       echo '  <td class="row1">
@@ -341,6 +401,9 @@
                 <div id="pg_create_catlink_2">
                   ' . $catlist . '
                 </div>
+                <div id="pg_create_regex_2">
+                  <input type="text" name="regex" size="60" /> 
+                </div>
               </td>
             </tr>';
             
@@ -911,6 +974,9 @@
         case PAGE_GRP_NORMAL:
           $type = 'Static set of pages';
           break;
+        case PAGE_GRP_REGEX:
+          $type = 'Regular expression match';
+          break;
       }
       $target = '';
       if ( $row['pg_type'] == PAGE_GRP_TAGGED )
@@ -921,6 +987,10 @@
       {
         $target = 'Category: ' . htmlspecialchars(get_page_title($paths->nslist['Category'] . sanitize_page_id($row['pg_target'])));
       }
+      else if ( $row['pg_type'] == PAGE_GRP_REGEX )
+      {
+        $target = 'Expression: <tt>' . htmlspecialchars($row['pg_target']) . '</tt>';
+      }
       $btn_edit = '<input type="submit" name="action[edit][' . $row['pg_id'] . ']" value="Edit" />';
       $btn_del = '<input type="submit" name="action[del][' . $row['pg_id'] . ']" value="Delete" />';
       // stupid jEdit bug/hack
--- a/plugins/admin/SecurityLog.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/plugins/admin/SecurityLog.php	Sun Oct 07 21:31:14 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  *
  * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
--- a/plugins/admin/UserManager.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/plugins/admin/UserManager.php	Sun Oct 07 21:31:14 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  *
  * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
--- a/schema.sql	Sun Sep 30 19:22:04 2007 -0400
+++ b/schema.sql	Sun Oct 07 21:31:14 2007 -0400
@@ -1,5 +1,5 @@
 -- Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
--- Version 1.0 (Banshee)
+-- Version 1.0.2 (Coblynau)
 -- Copyright (C) 2006-2007 Dan Fuhry
 
 -- This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
@@ -254,6 +254,16 @@
   PRIMARY KEY ( tag_id )
 ) CHARACTER SET `utf8`;
 
+-- Added in 1.1.1
+
+CREATE TABLE {{TABLE_PREFIX}}lockout(
+  id int(12) NOT NULL auto_increment,
+  ipaddr varchar(40) NOT NULL,
+  action ENUM('credential', 'level') NOT NULL DEFAULT 'credential',
+  timestamp int(12) NOT NULL DEFAULT 0,
+  PRIMARY KEY ( id )
+) CHARACTER SET `utf8`;
+
 INSERT INTO {{TABLE_PREFIX}}config(config_name, config_value) VALUES
   ('site_name', '{{SITE_NAME}}'),
   ('main_page', 'Main_Page'),
@@ -278,7 +288,7 @@
   ('copyright_notice', '{{COPYRIGHT}}'),
   ('wiki_edit_notice_text', '== Why can I edit this page? ==\n\nEveryone can edit almost any page in this website. This concept is called a wiki. It gives everyone the opportunity to make a change for the best. While some spam and vandalism may occur, it is believed that most contributions will be legitimate and helpful.\n\nFor security purposes, a history of all page edits is kept, and administrators are able to restore vandalized or spammed pages with just a few clicks.'),
   ('cache_thumbs', '{{ENABLE_CACHE}}'),
-  ('max_file_size', '256000'),('enano_version', '{{VERSION}}'),('enano_beta_version', '{{BETA_VERSION}}'),( 'allowed_mime_types', 'cbf:len=168;crc=c3dcad3f;data=0[1],1[4],0[3],1[1],0[2],1[1],0[11],1[1],0[7],1[1],0[9],1[1],0[6],1[3],0[10],1[1],0[2],1[2],0[1],1[1],0[1],1[2],0[6],1[3],0[1],1[1],0[2],1[4],0[1],1[2],0[3],1[1],0[4],1[2],0[26],1[5],0[6],1[2],0[2],1[1],0[4],1[1],0[10],1[2],0[1],1[1],0[6]|end' ),
+  ('max_file_size', '256000'),('enano_version', '{{VERSION}}'),( 'allowed_mime_types', 'cbf:len=168;crc=c3dcad3f;data=0[1],1[4],0[3],1[1],0[2],1[1],0[11],1[1],0[7],1[1],0[9],1[1],0[6],1[3],0[10],1[1],0[2],1[2],0[1],1[1],0[1],1[2],0[6],1[3],0[1],1[1],0[2],1[4],0[1],1[2],0[3],1[1],0[4],1[2],0[26],1[5],0[6],1[2],0[2],1[1],0[4],1[1],0[10],1[2],0[1],1[1],0[6]|end' ),
   ('contact_email', '{{ADMIN_EMAIL}}'),
   ('powered_btn', '1');
 
--- a/themes/oxygen/css/bleu.css	Sun Sep 30 19:22:04 2007 -0400
+++ b/themes/oxygen/css/bleu.css	Sun Oct 07 21:31:14 2007 -0400
@@ -6,7 +6,7 @@
  
 /* The basics */
 html,body                          { height: 100%; }
-body                               { margin: 0; padding: 0; background: url(../images/bleu/bg.png); font-family: trebuchet ms, verdana, arial, helvetica, sans-serif; font-size: 9pt; }
+body                               { /* color added in 1.0.2 to fix light text in dark desktop themes */ color: #202020; margin: 0; padding: 0; background: url(../images/bleu/bg.png); font-family: trebuchet ms, verdana, arial, helvetica, sans-serif; font-size: 9pt; }
 .holder                            { border: 1px solid #CCCCCC; padding: 1px; background-color: #FFFFFF; color: #444444 }
 div.pad                            { padding: 10px; }                         
 table#title                        { margin: 0; padding: 0; height: 100px; background-color: #90B0D0; text-align: center; }
--- a/themes/printable/css/default.css	Sun Sep 30 19:22:04 2007 -0400
+++ b/themes/printable/css/default.css	Sun Oct 07 21:31:14 2007 -0400
@@ -6,7 +6,7 @@
  
 /* The basics */
 html,body                          { height: 100%; }
-body                               { margin: 0; padding: 0; background-color: #FFFFFF; font-family: trebuchet ms, verdana, arial, helvetica, sans-serif; font-size: 9pt; }
+body                               { /* color added in 1.0.2 to fix light text in dark desktop themes */ color: #202020; margin: 0; padding: 0; background-color: #FFFFFF; font-family: trebuchet ms, verdana, arial, helvetica, sans-serif; font-size: 9pt; }
 .holder                            { border: 1px solid #CCCCCC; padding: 1px; background-color: #FFFFFF; color: #444444 }
 div.pad                            { padding: 10px; }                         
 table#title                        { margin: 0; padding: 0; height: 100px; background-color: #90B0D0; text-align: center; }
--- a/themes/stpatty/css/shamrock.css	Sun Sep 30 19:22:04 2007 -0400
+++ b/themes/stpatty/css/shamrock.css	Sun Oct 07 21:31:14 2007 -0400
@@ -16,6 +16,8 @@
 }
 body {
   background-color: #101d14;
+  /* color added in 1.0.2 to fix light text in dark desktop themes */
+  color: #202020;
   background-image: url(../images/bghatching.gif);
   background-repeat: repeat;
   font-family: "Lucida Sans Unicode", sans-serif;
--- a/upgrade.php	Sun Sep 30 19:22:04 2007 -0400
+++ b/upgrade.php	Sun Oct 07 21:31:14 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * upgrade.php - upgrade script
  * Copyright (C) 2006-2007 Dan Fuhry
  *
@@ -61,7 +61,7 @@
 // Everything related to versions goes here!
 
 // Valid versions to upgrade from
-$valid_versions = Array('1.0b1', '1.0b2', '1.0b3', '1.0b4', '1.0RC1', '1.0RC2', '1.0RC3', '1.0', '1.0.1', '1.0.1.1');
+$valid_versions = Array('1.0b1', '1.0b2', '1.0b3', '1.0b4', '1.0RC1', '1.0RC2', '1.0RC3', '1.0', '1.0.1', '1.0.1.1', '1.0.2b1', '1.0.2', 'Stable1.0ToUnstable1.1');
 
 // Basically a list of dependencies, which should be resolved automatically
 // If, for example, upgrading from 1.0b1 to 1.0RC1 requires one extra query that would not
@@ -75,9 +75,12 @@
     '1.0RC2' => Array('1.0RC3'),
     '1.0RC3' => Array('1.0'),
     '1.0' => Array('1.0.1'),
-    '1.0.1' => Array('1.0.1.1')
+    '1.0.1' => Array('1.0.1.1'),
+    '1.0.1.1' => Array('1.0.2b1'),
+    '1.0.2b1' => Array('Stable1.0ToUnstable1.1'),
+    'Stable1.0ToUnstable1.1' => Array('1.1.1')
   );
-$this_version   = '1.0.2b1';
+$this_version   = '1.1.1';
 $func_list = Array(
     '1.0' => Array('u_1_0_1_update_del_votes'),
     '1.0b4' => Array('u_1_0_RC1_update_user_ids', 'u_1_0_RC1_add_admins_to_group', 'u_1_0_RC1_alter_files_table', 'u_1_0_RC1_destroy_session_cookie', 'u_1_0_RC1_set_contact_email', 'u_1_0_RC1_update_page_text'), // ,
@@ -157,7 +160,7 @@
 $session->start();
 
 $template = new template_nodb();
-$template->load_theme('stpatty', 'shamrock', false);
+$template->load_theme('oxygen', 'bleu', false);
 
 $modestrings = Array(
               'login'      => 'Administrative login',
@@ -525,7 +528,7 @@
     ?>
     
     <div style="text-align: center; margin-top: 10px;">
-      <img alt="[ Enano CMS Project logo ]" src="images/enano-artwork/installer-greeting-green.png" style="display: block; margin: 0 auto; padding-left: 134px;" />
+      <img alt="[ Enano CMS Project logo ]" src="images/enano-artwork/installer-greeting-blue.png" style="display: block; margin: 0 auto; padding-left: 134px;" />
       <h2>Welcome to the Enano upgrade wizard</h2>
       <?php
       if ( file_exists('./_nightly.php') )
@@ -619,10 +622,10 @@
       $schema = file_get_contents('upgrade.sql');
       
       // Strip out and process version blocks
-      preg_match_all('#---BEGIN ([0-9A-z\.\-]*?)---'."\n".'(.*?)'."\n".'---END \\1---#is', $schema, $matches);
+      preg_match_all('#---BEGIN ([0-9A-z\.\-]*?)---'."\n".'((.*?)'."\n)?".'---END \\1---#is', $schema, $matches);
       
       $from_list  =& $matches[1];
-      $query_list =& $matches[2];
+      $query_list =& $matches[3];
       
       foreach($matches[0] as $m)
       {
--- a/upgrade.sql	Sun Sep 30 19:22:04 2007 -0400
+++ b/upgrade.sql	Sun Oct 07 21:31:14 2007 -0400
@@ -3,12 +3,22 @@
 -- ALL NON-SQL LINES, even otherwise blank lines, must start with "--" or they will get sent to MySQL!
 -- Common tasks (version numbers)
 DELETE FROM {{TABLE_PREFIX}}config WHERE config_name='enano_version' OR config_name='enano_beta_version' OR config_name='enano_alpha_version' OR config_name='enano_rc_version';
-INSERT INTO {{TABLE_PREFIX}}config (config_name, config_value) VALUES( 'enano_version', '1.0.2' ),( 'enano_beta_version', '1' );
+INSERT INTO {{TABLE_PREFIX}}config (config_name, config_value) VALUES( 'enano_version', '1.1.1' );
+---BEGIN Stable1.0ToUnstable1.1---
+-- UPDATE {{TABLE_PREFIX}}groups SET group_id=9998 WHERE group_id=4;
+-- UPDATE {{TABLE_PREFIX}}group_members SET group_id=9998 WHERE group_id=4;
+-- INSERT INTO {{TABLE_PREFIX}}groups(group_id,group_name,group_type,system_group) VALUES(4, 'Regular members', 3, 1);
+CREATE TABLE {{TABLE_PREFIX}}lockout( id int(12) NOT NULL auto_increment, ipaddr varchar(40) NOT NULL, action ENUM('credential', 'level') NOT NULL DEFAULT 'credential', timestamp int(12) NOT NULL DEFAULT 0, PRIMARY KEY ( id ) ) CHARACTER SET `utf8`;
+---END Stable1.0ToUnstable1.1---
+---BEGIN 1.0.2---
+---END 1.0.2---
+---BEGIN 1.0.2b1---
+-- This is really optional, but could reduce confusion if regex page groups get truncated for no apparent reason.
+ALTER TABLE {{TABLE_PREFIX}}page_groups MODIFY COLUMN pg_target text DEFAULT NULL;
+---END 1.0.2b1---
 ---BEGIN 1.0.1.1---
--- No changes in this release
 ---END 1.0.1.1---
 ---BEGIN 1.0.1---
--- No changes in this release
 ---END 1.0.1---
 ---BEGIN 1.0---
 -- Fix for obnoxious $_GET issue