Browse Source

First Commit

Nathaniel van Diepen 10 years ago
commit
1e4924a715

+ 22 - 0
.gitattributes

@@ -0,0 +1,22 @@
+# Auto detect text files and perform LF normalization
+* text=auto
+
+# Custom for Visual Studio
+*.cs     diff=csharp
+*.sln    merge=union
+*.csproj merge=union
+*.vbproj merge=union
+*.fsproj merge=union
+*.dbproj merge=union
+
+# Standard to msysgit
+*.doc	 diff=astextplain
+*.DOC	 diff=astextplain
+*.docx diff=astextplain
+*.DOCX diff=astextplain
+*.dot  diff=astextplain
+*.DOT  diff=astextplain
+*.pdf  diff=astextplain
+*.PDF	 diff=astextplain
+*.rtf	 diff=astextplain
+*.RTF	 diff=astextplain

+ 217 - 0
.gitignore

@@ -0,0 +1,217 @@
+#################
+## Eclipse
+#################
+
+*.pydevproject
+.project
+.metadata
+bin/
+tmp/
+*.tmp
+*.bak
+*.swp
+*~.nib
+local.properties
+.classpath
+.settings/
+.loadpath
+
+# External tool builders
+.externalToolBuilders/
+
+# Locally stored "Eclipse launch configurations"
+*.launch
+
+# CDT-specific
+.cproject
+
+# PDT-specific
+.buildpath
+
+
+#################
+## Visual Studio
+#################
+
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.sln.docstates
+
+# Build results
+
+[Dd]ebug/
+[Rr]elease/
+x64/
+build/
+[Bb]in/
+[Oo]bj/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+*_i.c
+*_p.c
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.log
+*.scc
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opensdf
+*.sdf
+*.cachefile
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+*.ncrunch*
+.*crunch*.local.xml
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.Publish.xml
+*.pubxml
+
+# NuGet Packages Directory
+## TODO: If you have NuGet Package Restore enabled, uncomment the next line
+#packages/
+
+# Windows Azure Build Output
+csx
+*.build.csdef
+
+# Windows Store app package directory
+AppPackages/
+
+# Others
+sql/
+*.Cache
+ClientBin/
+[Ss]tyle[Cc]op.*
+~$*
+*~
+*.dbmdl
+*.[Pp]ublish.xml
+*.pfx
+*.publishsettings
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file to a newer
+# Visual Studio version. Backup files are not needed, because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+App_Data/*.mdf
+App_Data/*.ldf
+
+#############
+## Windows detritus
+#############
+
+# Windows image file caches
+Thumbs.db
+ehthumbs.db
+
+# Folder config file
+Desktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Mac crap
+.DS_Store
+
+
+#############
+## Python
+#############
+
+*.py[co]
+
+# Packages
+*.egg
+*.egg-info
+dist/
+build/
+eggs/
+parts/
+var/
+sdist/
+develop-eggs/
+.installed.cfg
+
+# Installer logs
+pip-log.txt
+
+# Unit test / coverage reports
+.coverage
+.tox
+
+#Translations
+*.mo
+
+#Mr Developer
+.mr.developer.cfg
+config.example.php
+site/favicon.ico

+ 7 - 0
config.php

@@ -0,0 +1,7 @@
+<?php
+	define('MYSQL_SERVER','localhost');
+	define('MYSQL_USER','ircd');
+	define('MYSQL_PASSWORD','ircd');
+	define('MYSQL_DATABASE','ircd');
+	define('HOSTNAME','//localhost/');
+?>

+ 46 - 0
header.php

@@ -0,0 +1,46 @@
+<?php
+	error_reporting(E_ALL);
+	ini_set("display_errors", 1);
+	session_start();
+	define('DIR',dirname(__FILE__));
+	require_once(DIR.'/config.php');
+	require_once(DIR."/lib/irc.php");
+	require_once(DIR."/lib/security.php");
+	require_once(DIR."/lib/users.php");
+	require_once(DIR."/lib/servers.php");
+	require_once(DIR."/lib/opers.php");
+	require_once(DIR."/lib/forms.php");
+	require_once(DIR."/lib/configuration.php");
+	if(!empty($_SERVER['HTTP_CLIENT_IP'])){
+		$ip = $_SERVER['HTTP_CLIENT_IP'];
+	}elseif(!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
+		$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
+	}elseif(!empty($_SERVER['REMOTE_ADDR'])){
+		$ip = $_SERVER['REMOTE_ADDR'];
+	}else{
+		$ip = '';
+	}
+	define('USER_IP',$ip);
+	function get_sql(){
+		static $sql;
+		if(!$sql){
+			$sql = new mysqli(MYSQL_SERVER,MYSQL_USER,MYSQL_PASSWORD,MYSQL_DATABASE);
+			if ($sql->connect_errno) {
+				echo "Failed to connect to MySQL: (" . $sql->connect_errno . ") " . $sql->connect_error;
+				die();
+			}
+		}
+		return $sql;
+	}
+	function query($query,$args=Array()){
+		$sql = get_sql();
+		for ($i=0;$i<count($args);$i++){
+			if(is_string($args[$i])){
+				$args[$i] = $sql->real_escape_string($args[$i]);
+			}elseif(!is_numeric($args[$i])){
+				return false;
+			}
+		}
+		return $sql->query(vsprintf($query,$args));
+	}
+?>

+ 343 - 0
index.php

@@ -0,0 +1,343 @@
+<?php
+	header('Content-type: text/plain');
+	require_once('header.php');
+	if(!isset($_GET['user']) || !isset($_GET['key']) || !isset($_GET['server'])){
+		$opts = getopt('u:k:s:',Array('user:','key:','server:'));
+		$_GET['user'] = isset($opts['user'])?$opts['user']:(isset($opts['u'])?$opts['u']:false);
+		$_GET['key'] = isset($opts['key'])?$opts['key']:(isset($opts['k'])?$opts['k']:false);
+		$_GET['server'] = isset($opts['server'])?$opts['server']:(isset($opts['s'])?$opts['s']:false);
+		if(!$_GET['user'] || !$_GET['key'] || !$_GET['server']){
+			die('# Please provide a user, api key and a server name.');
+		}
+	}
+	$user = get_current_user_obj('netadmin') or $user = get_current_user_obj('servermanager') or $user = get_current_user_obj('globaladmin') or die('# Invalid user/key pair.');
+	$server = get_current_server_obj() or die('# Invalid server name');;
+	$opers = get_opers_for_server_obj($server['id']);
+	$pass = mkpasswd(get_conf('server-pass'));
+?>
+#################################################
+##                   Classes                   ##
+#################################################
+class		clients
+{
+	pingfreq 120;
+	maxclients 500;
+	sendq 100000;
+	recvq 8000;
+};
+class		servers
+{
+	pingfreq 120;
+	maxclients 11;
+	sendq 1000000;
+	connfreq 100;
+};
+#################################################
+##                     Me                      ##
+#################################################
+me {
+	name "<?php echo $server['host'];?>";
+	info "<?php echo $server['description'];?>";
+	numeric <?php echo $server['id'];?>;
+};
+#################################################
+##                   Admin                     ##
+#################################################
+admin {
+	"<?php echo $user['real_name'];?>";
+	"<?php echo $user['nick'];?>";
+	"<?php echo $user['email'];?>";
+};
+#################################################
+##                 Listeners                   ##
+#################################################
+listen         *:6697
+{
+	options
+	{
+		ssl;
+		clientsonly;
+	};
+};
+listen         *:8067;
+listen         *:6667;
+listen         *:6666;
+listen         *:6665;
+listen         *:7150
+{
+	options
+	{
+		serversonly;
+	};
+};
+listen         *:7100
+{
+	options
+	{
+		ssl;
+		serversonly;
+	};
+};
+#################################################
+##                   Link                      ##
+#################################################
+<?php
+	$ulines = get_ulines_obj();
+	foreach($ulines as $k => $u){?>
+link        <?php echo $u['host'];?> {
+	username *;
+	hostname *;
+	bind-ip *;
+	hub *;
+	port 7150;
+	password-receive "<?php echo $pass ?>" { sha1; };
+	password-connect "<?php echo get_conf('server-pass'); ?>";
+	class servers;
+};
+<?php
+	}
+	if(!is_null($server['parent'])){?>
+link        <?php echo $server['parent']['host'];?> {
+	username *;
+	hostname <?php echo $server['parent']['ip'];?>;
+	bind-ip *;
+	hub *;
+	port 7100;
+	password-receive "<?php echo $pass ?>" { sha1; };
+	password-connect "<?php echo get_conf('server-pass'); ?>";
+	class       servers;
+	options
+	{
+		zip;
+		ssl;
+		autoconnect;
+		nodnscache;
+		nohostcheck;
+	};
+};
+<?php
+	}
+	if(isset($server['children'])){
+		foreach($server['children'] as $k => $c){?>
+link        <?php echo $c['host'];?> {
+	username *;
+	hostname <?php echo $c['ip'];?>;
+	bind-ip *;
+	hub *;
+	port 7100;
+	password-receive "<?php echo $pass ?>" { sha1; };
+	password-connect "<?php echo get_conf('server-pass'); ?>";
+	class       servers;
+	options
+	{
+		zip;
+		ssl;
+		autoconnect;
+		nodnscache;
+		nohostcheck;
+	};
+};
+<?php		}
+	}
+?>
+ulines {
+	<?php
+		$ulines = get_ulines();
+		foreach($ulines as $k => $uline){
+			echo $uline.";\n";
+			if($k < count($ulines)-1){
+				echo "\t";
+			}
+		}
+	?>
+};
+#################################################
+##                   Log                       ##
+#################################################
+log "ircd.log" {
+	flags {
+		oper;
+		kline;
+		connects;
+		server-connects;
+		kills;
+		errors;
+		sadmin-commands;
+		chg-commands;
+		oper-override;
+		spamfilter;
+	};
+};
+#################################################
+##                   Alias                     ##
+#################################################
+alias "glinebot" {
+	format ".+" {
+		command "gline";
+		type real;
+		parameters "%1 2d Bots are not allowed on this server, please read the faq at http://www.example.com/faq/123";
+	};
+	type command;
+};
+alias statserv { type stats; };
+alias ss { target statserv; type stats; };
+#################################################
+##                   DRPass                    ##
+#################################################
+drpass {
+	restart "<?php echo $pass ?>" { sha1; };
+	die "<?php echo $pass ?>" { sha1; };
+};
+#################################################
+##             Network Settings                ##
+#################################################
+set {
+	network-name 		"omnimaga.org";
+	default-server 		"irc.omnimaga.org";
+	services-server 	"<?php echo get_conf('services-server','string'); ?>";
+	stats-server		"<?php echo get_conf('stats-server','string'); ?>";
+	help-channel 		"#omnimaga";
+	hiddenhost-prefix	"omni";
+	cloak-keys {
+		"XFGasdgREWhgreTG43FDSfweqfew";
+		"FDSAyh5ghREFadhrGHrewGQEg324";
+		"ASGfdah4431fgdsagdsagASgrw32";
+	};
+	hosts {
+		local			"local.users.irc.omnimaga.org";
+		global			"global.users.irc.omnimaga.org";
+		coadmin			"coadmin.users.irc.omnimaga.org";
+		admin			"admin.users.irc.omnimaga.org";
+		servicesadmin	"servicesadmin.users.irc.omnimaga.org";
+		netadmin 		"netadmin.users.irc.omnimaga.org";
+		host-on-oper-up "yes";
+	};
+	modes-on-join		"+nt";
+	kline-address "[email protected]";
+	modes-on-connect "+G";
+	modes-on-oper	 "+wgs";
+	oper-auto-join "<?php echo get_conf('ops-channel','string'); ?>";
+	options {
+		hide-ulines;
+		show-connect-info;
+	};
+	maxchannelsperuser 50;
+	anti-spam-quit-message-time 10s;
+	oper-only-stats "okfGsMRUEelLCXzdD";
+	throttle {
+		connections 3;
+		period 60s;
+	};
+	anti-flood {
+		nick-flood 3:60;
+	};
+	spamfilter {
+		ban-time 1d;
+		ban-reason "Spam/Advertising";
+		virus-help-channel "#help";
+	};
+};
+#################################################
+##                Enable Mibbit                ##
+#################################################
+// Datacenter one:
+cgiirc {
+	type webirc;
+	hostname 64.62.228.82;
+	password <?php echo get_conf('mibbit-password','string'); ?>;
+};
+// Datacenter two:
+cgiirc {
+	type webirc;
+	hostname 207.192.75.252;
+	password <?php echo get_conf('mibbit-password','string'); ?>;
+};
+// Datacenter three:
+cgiirc {
+	type webirc;
+	hostname 78.129.202.38;
+	password <?php echo get_conf('mibbit-password','string'); ?>;
+};
+// Datacenter four:
+cgiirc {
+	type webirc;
+	hostname 109.169.29.95;
+	password <?php echo get_conf('mibbit-password','string'); ?>;
+};
+#################################################
+##                    Allow                    ##
+#################################################
+allow {
+	ip             *@*;
+	hostname       *@*;
+	class           clients;
+	maxperip	10;
+};
+#################################################
+##                     Deny                    ##
+#################################################
+deny dcc {
+	filename "*sub7*";
+	reason "Possible Sub7 Virus";
+};
+#################################################
+##                    Bans                     ##
+#################################################
+ban nick {
+	mask "*C*h*a*n*S*e*r*v*";
+	reason "Reserved for Services";
+};
+#################################################
+##                Localization                 ##
+#################################################
+files {
+	motd "motd/en.txt";
+	rules "rules/en.txt";
+};
+tld {
+	mask *@*.ca;
+	motd "motd/en_CA.txt";
+	rules "rules/en_CA.txt";
+};
+tld {
+	mask *@*.com;
+	motd "motd/en_US.txt";
+	rules "rules/en_US.txt";
+};
+tld {
+	mask *@*.fr;
+	motd "motd/fr.txt";
+	rules "rules/fr.txt";
+};
+#################################################
+##                    Opers                    ##
+#################################################
+oper RehashServ {
+	class		clients;
+	from {
+		userhost RehashServ@localhost;
+		userhost [email protected];
+		userhost [email protected];
+	};
+	password "<?php echo mkpasswd(get_conf('rehash-pass')); ?>" { sha1; };
+	flags {
+		can_rehash;
+		netadmin;
+	};
+};
+
+<?php foreach($opers as $k => $oper){?>
+oper <?php echo $oper['nick'];?> {
+	class		clients;
+	from {
+		<?php foreach($oper['hosts'] as $k => $host){?>
+			userhost <?php echo $host;?>;
+		<?php } ?>
+	};
+	password "<?php echo $oper['password'];?>" { <?php echo $oper['password_type'];?>; };
+	flags {
+		<?php echo $oper['flags'];?>
+	};
+	swhois "<?php echo $oper['swhois'];?>";
+};
+<?php }?>

+ 525 - 0
ircd.sql

@@ -0,0 +1,525 @@
+-- phpMyAdmin SQL Dump
+-- version 4.0.4.1
+-- http://www.phpmyadmin.net
+--
+-- Host: 127.0.0.1
+-- Generation Time: Feb 20, 2014 at 02:10 AM
+-- Server version: 5.6.11
+-- PHP Version: 5.5.3
+
+SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
+SET AUTOCOMMIT = 0;
+START TRANSACTION;
+SET time_zone = "+00:00";
+
+
+/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
+/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
+/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
+/*!40101 SET NAMES utf8 */;
+
+--
+-- Database: `ircd`
+--
+CREATE DATABASE IF NOT EXISTS `ircd` DEFAULT CHARACTER SET latin1 COLLATE latin1_general_ci;
+USE `ircd`;
+
+-- --------------------------------------------------------
+
+--
+-- Stand-in structure for view `children_v`
+--
+DROP VIEW IF EXISTS `children_v`;
+CREATE TABLE IF NOT EXISTS `children_v` (
+`user_id` int(100)
+,`parent_id` int(100)
+,`child_id` int(100)
+);
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `configuration`
+--
+-- Creation: Feb 19, 2014 at 10:35 PM
+--
+
+DROP TABLE IF EXISTS `configuration`;
+CREATE TABLE IF NOT EXISTS `configuration` (
+  `key` varchar(100) CHARACTER SET latin1 COLLATE latin1_general_ci NOT NULL,
+  `description` varchar(100) NOT NULL,
+  `value` varchar(4000) DEFAULT NULL,
+  `type` varchar(100) NOT NULL,
+  PRIMARY KEY (`key`),
+  UNIQUE KEY `key` (`key`),
+  KEY `key_2` (`key`)
+) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+
+--
+-- Dumping data for table `configuration`
+--
+
+INSERT INTO `configuration` (`key`, `description`, `value`, `type`) VALUES
+('2-factor-method', '2-Factor Method', 'none', 'list'),
+('authy-api-key', 'Authy API Key', NULL, 'string'),
+('authy-endpoint', 'Authy Endpoint', 'http://sandbox-api.authy.com', 'list'),
+('irc-port', 'Main Server Port', '6667', 'number'),
+('irc-server', 'Main Server IP', NULL, 'lookup'),
+('mibbit-password', 'Mibbit Password', NULL, 'string'),
+('ops-channel', 'Opers Channel', '#opers', 'string'),
+('persona-audience', 'Persona Audience', NULL, 'string'),
+('persona-endpoint', 'Persona Endpoint', 'none', 'list'),
+('rehash-pass', 'RehashServ Password', NULL, 'string'),
+('server-pass', 'Server-to-Server Password', NULL, 'string'),
+('services-server', 'Services Server', NULL, 'lookup'),
+('stats-server', 'Stats Server', NULL, 'lookup'),
+('xmlrpc-path', 'XMLRPC Path', '/xmlrpc', 'string'),
+('xmlrpc-port', 'XMLRPC Port', '9900', 'number'),
+('xmlrpc-server', 'XMLRPC Server', NULL, 'lookup');
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `configuration_lists`
+--
+-- Creation: Feb 19, 2014 at 10:35 PM
+--
+
+DROP TABLE IF EXISTS `configuration_lists`;
+CREATE TABLE IF NOT EXISTS `configuration_lists` (
+  `id` int(100) NOT NULL AUTO_INCREMENT,
+  `key` varchar(100) COLLATE latin1_general_ci NOT NULL,
+  `value` varchar(4000) COLLATE latin1_general_ci NOT NULL,
+  `label` varchar(100) COLLATE latin1_general_ci NOT NULL,
+  PRIMARY KEY (`id`),
+  KEY `key` (`key`)
+) ENGINE=InnoDB  DEFAULT CHARSET=latin1 COLLATE=latin1_general_ci AUTO_INCREMENT=8 ;
+
+--
+-- RELATIONS FOR TABLE `configuration_lists`:
+--   `key`
+--       `configuration` -> `key`
+--
+
+--
+-- Dumping data for table `configuration_lists`
+--
+
+INSERT INTO `configuration_lists` (`id`, `key`, `value`, `label`) VALUES
+(1, '2-factor-method', 'none', '(none)'),
+(2, '2-factor-method', 'google-authenticator', 'Google Authenticator'),
+(3, '2-factor-method', 'authy', 'Authy'),
+(4, 'authy-endpoint', 'https://api.authy.com', 'Production'),
+(5, 'authy-endpoint', 'http://sandbox-api.authy.com', 'Sandbox'),
+(6, 'persona-endpoint', 'none', '(none)'),
+(7, 'persona-endpoint', 'https://verifier.login.persona.org/verify', 'Production');
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `configuration_lookups`
+--
+-- Creation: Feb 19, 2014 at 10:35 PM
+--
+
+DROP TABLE IF EXISTS `configuration_lookups`;
+CREATE TABLE IF NOT EXISTS `configuration_lookups` (
+  `id` int(100) NOT NULL AUTO_INCREMENT,
+  `key` varchar(100) COLLATE latin1_general_ci NOT NULL,
+  `table` varchar(100) COLLATE latin1_general_ci NOT NULL,
+  `column` varchar(100) COLLATE latin1_general_ci NOT NULL,
+  `label_column` varchar(100) COLLATE latin1_general_ci NOT NULL,
+  `enabled_column` varchar(100) COLLATE latin1_general_ci DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `configuration_key` (`key`)
+) ENGINE=InnoDB  DEFAULT CHARSET=latin1 COLLATE=latin1_general_ci AUTO_INCREMENT=5 ;
+
+--
+-- RELATIONS FOR TABLE `configuration_lookups`:
+--   `key`
+--       `configuration` -> `key`
+--
+
+--
+-- Dumping data for table `configuration_lookups`
+--
+
+INSERT INTO `configuration_lookups` (`id`, `key`, `table`, `column`, `label_column`, `enabled_column`) VALUES
+(1, 'irc-server', 'servers', 'ip', 'name', '!uline'),
+(2, 'services-server', 'servers', 'host', 'name', 'uline'),
+(3, 'stats-server', 'servers', 'host', 'name', 'uline'),
+(4, 'xmlrpc-server', 'servers', 'ip', 'name', 'uline');
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `emails`
+--
+-- Creation: Feb 19, 2014 at 10:35 PM
+--
+
+DROP TABLE IF EXISTS `emails`;
+CREATE TABLE IF NOT EXISTS `emails` (
+  `id` int(100) NOT NULL AUTO_INCREMENT,
+  `user_id` int(100) NOT NULL,
+  `email` varchar(100) COLLATE latin1_general_ci NOT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `email` (`email`),
+  KEY `user_id` (`user_id`)
+) ENGINE=InnoDB  DEFAULT CHARSET=latin1 COLLATE=latin1_general_ci AUTO_INCREMENT=16 ;
+
+--
+-- RELATIONS FOR TABLE `emails`:
+--   `user_id`
+--       `users` -> `id`
+--
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `hosts`
+--
+-- Creation: Feb 19, 2014 at 10:35 PM
+--
+
+DROP TABLE IF EXISTS `hosts`;
+CREATE TABLE IF NOT EXISTS `hosts` (
+  `id` int(100) NOT NULL AUTO_INCREMENT,
+  `oper_id` int(100) NOT NULL,
+  `host` varchar(100) NOT NULL,
+  PRIMARY KEY (`id`),
+  KEY `oper_id` (`oper_id`)
+) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=20 ;
+
+--
+-- RELATIONS FOR TABLE `hosts`:
+--   `oper_id`
+--       `opers` -> `id`
+--
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `opers`
+--
+-- Creation: Feb 19, 2014 at 10:35 PM
+--
+
+DROP TABLE IF EXISTS `opers`;
+CREATE TABLE IF NOT EXISTS `opers` (
+  `id` int(100) NOT NULL AUTO_INCREMENT,
+  `nick` varchar(20) NOT NULL,
+  `role_id` int(11) NOT NULL,
+  `user_id` int(100) NOT NULL,
+  `manager_id` int(100) NOT NULL,
+  `server_id` int(100) DEFAULT NULL,
+  `password` varchar(100) NOT NULL,
+  `password_type_id` int(100) NOT NULL,
+  `swhois` varchar(100) DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `id` (`id`),
+  UNIQUE KEY `nick` (`nick`),
+  KEY `id_2` (`id`),
+  KEY `nick_2` (`nick`),
+  KEY `role_id` (`role_id`),
+  KEY `password_type_id` (`password_type_id`),
+  KEY `user_id` (`manager_id`),
+  KEY `server_id` (`server_id`),
+  KEY `user_id_2` (`user_id`)
+) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=9 ;
+
+--
+-- RELATIONS FOR TABLE `opers`:
+--   `password_type_id`
+--       `password_type` -> `id`
+--   `server_id`
+--       `servers` -> `id`
+--   `role_id`
+--       `oper_roles` -> `id`
+--   `manager_id`
+--       `users` -> `id`
+--
+
+-- --------------------------------------------------------
+
+--
+-- Stand-in structure for view `opers_v`
+--
+DROP VIEW IF EXISTS `opers_v`;
+CREATE TABLE IF NOT EXISTS `opers_v` (
+`id` int(100)
+,`user_id` int(100)
+,`manager_id` int(100)
+,`server_id` int(100)
+,`nick` varchar(20)
+,`password` varchar(100)
+,`password_type` varchar(100)
+,`swhois` varchar(100)
+,`flags` varchar(4000)
+,`role` varchar(20)
+);
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `oper_roles`
+--
+-- Creation: Feb 19, 2014 at 10:35 PM
+--
+
+DROP TABLE IF EXISTS `oper_roles`;
+CREATE TABLE IF NOT EXISTS `oper_roles` (
+  `id` int(100) NOT NULL AUTO_INCREMENT,
+  `name` varchar(20) NOT NULL,
+  `flags` varchar(4000) NOT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `name` (`name`)
+) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=4 ;
+
+--
+-- Dumping data for table `oper_roles`
+--
+
+INSERT INTO `oper_roles` (`id`, `name`, `flags`) VALUES
+(1, 'netadmin', 'netadmin;\ncan_restart;\ncan_die;\ncan_gkline;\ncan_zline;\ncan_gzline;\ncan_override;\ncan_addline;\nget_host;'),
+(2, 'global', 'global;\ncan_override;\ncan_setq;\ncan_addline;\ncan_dccdeny;\nget_host;'),
+(3, 'servicesadmin', 'services-admin;\r\ncan_override;\r\ncan_setq;\r\ncan_addline;\r\ncan_dccdeny;\r\nget_host;');
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `password_type`
+--
+-- Creation: Feb 19, 2014 at 10:35 PM
+--
+
+DROP TABLE IF EXISTS `password_type`;
+CREATE TABLE IF NOT EXISTS `password_type` (
+  `id` int(100) NOT NULL AUTO_INCREMENT,
+  `name` varchar(100) NOT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `name` (`name`)
+) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=3 ;
+
+--
+-- Dumping data for table `password_type`
+--
+
+INSERT INTO `password_type` (`id`, `name`) VALUES
+(1, 'md5'),
+(2, 'sha1');
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `servers`
+--
+-- Creation: Feb 19, 2014 at 10:35 PM
+--
+
+DROP TABLE IF EXISTS `servers`;
+CREATE TABLE IF NOT EXISTS `servers` (
+  `id` int(100) NOT NULL AUTO_INCREMENT,
+  `name` varchar(100) NOT NULL,
+  `host` varchar(100) NOT NULL,
+  `description` varchar(4000) NOT NULL,
+  `parent_id` int(100) DEFAULT NULL,
+  `user_id` int(100) NOT NULL,
+  `ip` varchar(15) NOT NULL,
+  `uline` tinyint(1) NOT NULL DEFAULT '0',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `name` (`name`,`host`),
+  KEY `parent_id` (`parent_id`),
+  KEY `user_id` (`user_id`)
+) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=9 ;
+
+--
+-- RELATIONS FOR TABLE `servers`:
+--   `parent_id`
+--       `servers` -> `id`
+--   `user_id`
+--       `users` -> `id`
+--
+
+-- --------------------------------------------------------
+
+--
+-- Stand-in structure for view `ulines_v`
+--
+DROP VIEW IF EXISTS `ulines_v`;
+CREATE TABLE IF NOT EXISTS `ulines_v` (
+`id` int(100)
+,`host` varchar(100)
+);
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `users`
+--
+-- Creation: Feb 19, 2014 at 10:35 PM
+--
+
+DROP TABLE IF EXISTS `users`;
+CREATE TABLE IF NOT EXISTS `users` (
+  `id` int(100) NOT NULL AUTO_INCREMENT,
+  `api_key` varchar(24) NOT NULL,
+  `secret_key` varchar(100) DEFAULT NULL,
+  `password` varchar(40) NOT NULL,
+  `real_name` varchar(50) NOT NULL,
+  `nick` varchar(20) CHARACTER SET latin1 COLLATE latin1_general_ci NOT NULL,
+  `email` varchar(100) NOT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `api_key` (`api_key`),
+  UNIQUE KEY `nick` (`nick`),
+  KEY `authy_id` (`secret_key`)
+) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=8 ;
+
+--
+-- Dumping data for table `users`
+--
+
+INSERT INTO `users` (`id`, `api_key`, `secret_key`, `password`, `real_name`, `nick`, `email`) VALUES
+(7, '1', NULL, '$Dj94pkis$Fs5kyCo4ocTT7zh8asWNJwIelP0=', 'root', 'root', '');
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `user_roles`
+--
+-- Creation: Feb 19, 2014 at 10:35 PM
+--
+
+DROP TABLE IF EXISTS `user_roles`;
+CREATE TABLE IF NOT EXISTS `user_roles` (
+  `id` int(100) NOT NULL AUTO_INCREMENT,
+  `user_id` int(100) NOT NULL,
+  `user_role_id` int(100) NOT NULL,
+  PRIMARY KEY (`id`),
+  KEY `user_id` (`user_id`,`user_role_id`),
+  KEY `user_role_id` (`user_role_id`)
+) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=14 ;
+
+--
+-- RELATIONS FOR TABLE `user_roles`:
+--   `user_id`
+--       `users` -> `id`
+--   `user_role_id`
+--       `user_role_types` -> `id`
+--
+
+--
+-- Dumping data for table `user_roles`
+--
+
+INSERT INTO `user_roles` (`id`, `user_id`, `user_role_id`) VALUES
+(13, 7, 4);
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `user_role_types`
+--
+-- Creation: Feb 19, 2014 at 10:35 PM
+--
+
+DROP TABLE IF EXISTS `user_role_types`;
+CREATE TABLE IF NOT EXISTS `user_role_types` (
+  `id` int(100) NOT NULL AUTO_INCREMENT,
+  `name` varchar(100) NOT NULL,
+  `description` varchar(100) NOT NULL,
+  `flags` varchar(3) NOT NULL DEFAULT 'o',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `name` (`name`),
+  UNIQUE KEY `description` (`description`)
+) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=5 ;
+
+--
+-- Dumping data for table `user_role_types`
+--
+
+INSERT INTO `user_role_types` (`id`, `name`, `description`, `flags`) VALUES
+(1, 'oper', 'Oper', 'o'),
+(2, 'admin', 'Server Manager', 'n'),
+(3, 'netadmin', 'Network Admin', 'on'),
+(4, 'globaladmin', 'Global Admin', 'nao');
+
+-- --------------------------------------------------------
+
+--
+-- Structure for view `children_v`
+--
+DROP TABLE IF EXISTS `children_v`;
+
+CREATE ALGORITHM=UNDEFINED DEFINER=`eeems`@`localhost` SQL SECURITY DEFINER VIEW `children_v` AS select `p`.`user_id` AS `user_id`,`p`.`id` AS `parent_id`,`c`.`id` AS `child_id` from (`servers` `c` left join `servers` `p` on((`p`.`id` = `c`.`parent_id`))) where ((`p`.`user_id` is not null) and (`c`.`user_id` is not null) and (`c`.`parent_id` is not null) and (`c`.`uline` = 0));
+
+-- --------------------------------------------------------
+
+--
+-- Structure for view `opers_v`
+--
+DROP TABLE IF EXISTS `opers_v`;
+
+CREATE ALGORITHM=UNDEFINED DEFINER=`eeems`@`localhost` SQL SECURITY DEFINER VIEW `opers_v` AS select `o`.`id` AS `id`,`o`.`user_id` AS `user_id`,`o`.`manager_id` AS `manager_id`,`o`.`server_id` AS `server_id`,`o`.`nick` AS `nick`,`o`.`password` AS `password`,`p`.`name` AS `password_type`,`o`.`swhois` AS `swhois`,`r`.`flags` AS `flags`,`r`.`name` AS `role` from ((`opers` `o` join `oper_roles` `r` on((`r`.`id` = `o`.`role_id`))) join `password_type` `p` on((`p`.`id` = `o`.`password_type_id`)));
+
+-- --------------------------------------------------------
+
+--
+-- Structure for view `ulines_v`
+--
+DROP TABLE IF EXISTS `ulines_v`;
+
+CREATE ALGORITHM=UNDEFINED DEFINER=`eeems`@`localhost` SQL SECURITY DEFINER VIEW `ulines_v` AS select `s`.`id` AS `id`,`s`.`host` AS `host` from `servers` `s` where (`s`.`uline` = 1);
+
+--
+-- Constraints for dumped tables
+--
+
+--
+-- Constraints for table `configuration_lists`
+--
+ALTER TABLE `configuration_lists`
+  ADD CONSTRAINT `configuration_lists_ibfk_1` FOREIGN KEY (`key`) REFERENCES `configuration` (`key`) ON DELETE CASCADE ON UPDATE CASCADE;
+
+--
+-- Constraints for table `configuration_lookups`
+--
+ALTER TABLE `configuration_lookups`
+  ADD CONSTRAINT `configuration_lookups_ibfk_1` FOREIGN KEY (`key`) REFERENCES `configuration` (`key`) ON DELETE CASCADE ON UPDATE CASCADE;
+
+--
+-- Constraints for table `emails`
+--
+ALTER TABLE `emails`
+  ADD CONSTRAINT `emails_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
+
+--
+-- Constraints for table `hosts`
+--
+ALTER TABLE `hosts`
+  ADD CONSTRAINT `hosts_ibfk_1` FOREIGN KEY (`oper_id`) REFERENCES `opers` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
+
+--
+-- Constraints for table `opers`
+--
+ALTER TABLE `opers`
+  ADD CONSTRAINT `opers_ibfk_2` FOREIGN KEY (`password_type_id`) REFERENCES `password_type` (`id`),
+  ADD CONSTRAINT `opers_ibfk_4` FOREIGN KEY (`server_id`) REFERENCES `servers` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
+  ADD CONSTRAINT `opers_ibfk_5` FOREIGN KEY (`role_id`) REFERENCES `oper_roles` (`id`),
+  ADD CONSTRAINT `opers_ibfk_6` FOREIGN KEY (`manager_id`) REFERENCES `users` (`id`);
+
+--
+-- Constraints for table `servers`
+--
+ALTER TABLE `servers`
+  ADD CONSTRAINT `servers_ibfk_1` FOREIGN KEY (`parent_id`) REFERENCES `servers` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
+  ADD CONSTRAINT `servers_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
+
+--
+-- Constraints for table `user_roles`
+--
+ALTER TABLE `user_roles`
+  ADD CONSTRAINT `user_roles_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
+  ADD CONSTRAINT `user_roles_ibfk_2` FOREIGN KEY (`user_role_id`) REFERENCES `user_role_types` (`id`) ON UPDATE CASCADE;
+COMMIT;
+
+/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
+/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
+/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

+ 201 - 0
lib/GoogleAuthenticator.php

@@ -0,0 +1,201 @@
+<?php
+
+/**
+ * PHP Class for handling Google Authenticator 2-factor authentication
+ *
+ * @author Michael Kliewe
+ * @copyright 2012 Michael Kliewe
+ * @license http://www.opensource.org/licenses/bsd-license.php BSD License
+ * @link http://www.phpgangsta.de/
+ */
+
+class PHPGangsta_GoogleAuthenticator
+{
+    protected $_codeLength = 6;
+
+    /**
+     * Create new secret.
+     * 16 characters, randomly chosen from the allowed base32 characters.
+     *
+     * @param int $secretLength
+     * @return string
+     */
+    public function createSecret($secretLength = 16)
+    {
+        $validChars = $this->_getBase32LookupTable();
+        unset($validChars[32]);
+
+        $secret = '';
+        for ($i = 0; $i < $secretLength; $i++) {
+            $secret .= $validChars[array_rand($validChars)];
+        }
+        return $secret;
+    }
+
+    /**
+     * Calculate the code, with given secret and point in time
+     *
+     * @param string $secret
+     * @param int|null $timeSlice
+     * @return string
+     */
+    public function getCode($secret, $timeSlice = null)
+    {
+        if ($timeSlice === null) {
+            $timeSlice = floor(time() / 30);
+        }
+
+        $secretkey = $this->_base32Decode($secret);
+
+        // Pack time into binary string
+        $time = chr(0).chr(0).chr(0).chr(0).pack('N*', $timeSlice);
+        // Hash it with users secret key
+        $hm = hash_hmac('SHA1', $time, $secretkey, true);
+        // Use last nipple of result as index/offset
+        $offset = ord(substr($hm, -1)) & 0x0F;
+        // grab 4 bytes of the result
+        $hashpart = substr($hm, $offset, 4);
+
+        // Unpak binary value
+        $value = unpack('N', $hashpart);
+        $value = $value[1];
+        // Only 32 bits
+        $value = $value & 0x7FFFFFFF;
+
+        $modulo = pow(10, $this->_codeLength);
+        return str_pad($value % $modulo, $this->_codeLength, '0', STR_PAD_LEFT);
+    }
+
+    /**
+     * Get QR-Code URL for image, from google charts
+     *
+     * @param string $name
+     * @param string $secret
+     * @return string
+     */
+    public function getQRCodeGoogleUrl($name, $secret) {
+        $urlencoded = urlencode('otpauth://totp/'.$name.'?secret='.$secret.'');
+        return 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl='.$urlencoded.'';
+    }
+
+    /**
+     * Check if the code is correct. This will accept codes starting from $discrepancy*30sec ago to $discrepancy*30sec from now
+     *
+     * @param string $secret
+     * @param string $code
+     * @param int $discrepancy This is the allowed time drift in 30 second units (8 means 4 minutes before or after)
+     * @return bool
+     */
+    public function verifyCode($secret, $code, $discrepancy = 1)
+    {
+        $currentTimeSlice = floor(time() / 30);
+
+        for ($i = -$discrepancy; $i <= $discrepancy; $i++) {
+            $calculatedCode = $this->getCode($secret, $currentTimeSlice + $i);
+            if ($calculatedCode == $code ) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Set the code length, should be >=6
+     *
+     * @param int $length
+     * @return PHPGangsta_GoogleAuthenticator
+     */
+    public function setCodeLength($length)
+    {
+        $this->_codeLength = $length;
+        return $this;
+    }
+
+    /**
+     * Helper class to decode base32
+     *
+     * @param $secret
+     * @return bool|string
+     */
+    protected function _base32Decode($secret)
+    {
+        if (empty($secret)) return '';
+
+        $base32chars = $this->_getBase32LookupTable();
+        $base32charsFlipped = array_flip($base32chars);
+
+        $paddingCharCount = substr_count($secret, $base32chars[32]);
+        $allowedValues = array(6, 4, 3, 1, 0);
+        if (!in_array($paddingCharCount, $allowedValues)) return false;
+        for ($i = 0; $i < 4; $i++){
+            if ($paddingCharCount == $allowedValues[$i] &&
+                substr($secret, -($allowedValues[$i])) != str_repeat($base32chars[32], $allowedValues[$i])) return false;
+        }
+        $secret = str_replace('=','', $secret);
+        $secret = str_split($secret);
+        $binaryString = "";
+        for ($i = 0; $i < count($secret); $i = $i+8) {
+            $x = "";
+            if (!in_array($secret[$i], $base32chars)) return false;
+            for ($j = 0; $j < 8; $j++) {
+                $x .= str_pad(base_convert(@$base32charsFlipped[@$secret[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT);
+            }
+            $eightBits = str_split($x, 8);
+            for ($z = 0; $z < count($eightBits); $z++) {
+                $binaryString .= ( ($y = chr(base_convert($eightBits[$z], 2, 10))) || ord($y) == 48 ) ? $y:"";
+            }
+        }
+        return $binaryString;
+    }
+
+    /**
+     * Helper class to encode base32
+     *
+     * @param string $secret
+     * @param bool $padding
+     * @return string
+     */
+    protected function _base32Encode($secret, $padding = true)
+    {
+        if (empty($secret)) return '';
+
+        $base32chars = $this->_getBase32LookupTable();
+
+        $secret = str_split($secret);
+        $binaryString = "";
+        for ($i = 0; $i < count($secret); $i++) {
+            $binaryString .= str_pad(base_convert(ord($secret[$i]), 10, 2), 8, '0', STR_PAD_LEFT);
+        }
+        $fiveBitBinaryArray = str_split($binaryString, 5);
+        $base32 = "";
+        $i = 0;
+        while ($i < count($fiveBitBinaryArray)) {
+            $base32 .= $base32chars[base_convert(str_pad($fiveBitBinaryArray[$i], 5, '0'), 2, 10)];
+            $i++;
+        }
+        if ($padding && ($x = strlen($binaryString) % 40) != 0) {
+            if ($x == 8) $base32 .= str_repeat($base32chars[32], 6);
+            elseif ($x == 16) $base32 .= str_repeat($base32chars[32], 4);
+            elseif ($x == 24) $base32 .= str_repeat($base32chars[32], 3);
+            elseif ($x == 32) $base32 .= $base32chars[32];
+        }
+        return $base32;
+    }
+
+    /**
+     * Get array with all 32 characters for decoding from/encoding to base32
+     *
+     * @return array
+     */
+    protected function _getBase32LookupTable()
+    {
+        return array(
+            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', //  7
+            'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15
+            'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23
+            'Y', 'Z', '2', '3', '4', '5', '6', '7', // 31
+            '='  // padding char
+        );
+    }
+}

+ 49 - 0
lib/authy-php/Authy.php

@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * Autoloader
+ *
+ * PHP version 5
+ *
+ * @category Services
+ * @package  Authy
+ * @author   David Cuadrado <[email protected]>
+ * @license  http://creativecommons.org/licenses/MIT/ MIT
+ * @link     http://authy.github.com/pear
+ */
+
+/**
+ * Autoloads Authy API files
+ * Based on https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md
+ *
+ * @param string $className class to load
+ *
+ * @return boolean true when the file was loaded
+ */
+function Authy_autoloader($className)
+{
+    $className = ltrim($className, '\\');
+    $baseDir  = __DIR__.'/lib/';
+    $fileName  = '';
+    $namespace = '';
+    if ($lastNsPos = strripos($className, '\\')) {
+        $namespace = substr($className, 0, $lastNsPos);
+        $className = substr($className, $lastNsPos + 1);
+        $fileName  = str_replace('\\', '/', $namespace) . '/';
+    }
+    $fileName .= str_replace('_', '/', $className) . '.php';
+
+    if (file_exists($baseDir.'/'.$fileName)) {
+        include $baseDir.'/'.$fileName;
+        return true;
+    } else if (file_exists($baseDir.'/vendor/'.$fileName)) {
+        include $baseDir.'/vendor/'.$fileName;
+        return true;
+    } else {
+        print("File not found for ". $className .": ".$fileName);
+    }
+
+    return false;
+}
+
+spl_autoload_register('Authy_autoloader');

+ 157 - 0
lib/authy-php/lib/Authy/Api.php

@@ -0,0 +1,157 @@
+<?php
+/**
+ * ApiClient
+ *
+ * PHP version 5
+ *
+ * @category Services
+ * @package  Authy
+ * @author   David Cuadrado <[email protected]>
+ * @license  http://creativecommons.org/licenses/MIT/ MIT
+ * @link     http://authy.github.com/pear
+ */
+
+/**
+ * Authy API interface.
+ *
+ * @category Services
+ * @package  Authy
+ * @author   David Cuadrado <[email protected]>
+ * @license  http://creativecommons.org/licenses/MIT/ MIT
+ * @link     http://authy.github.com/pear
+ */
+class Authy_Api
+{
+    const VERSION = '1.3.0';
+    protected $rest;
+    protected $api_key;
+    protected $api_url;
+
+    /**
+     * Constructor.
+     *
+     * @param string $api_key Api Key
+     * @param string $api_url Optional api url
+     */
+    public function __construct($api_key, $api_url = "https://api.authy.com")
+    {
+        $this->rest = new Resty();
+        $this->rest->setBaseURL($api_url);
+        $this->rest->setUserAgent("authy-php v".Authy_Api::VERSION);
+
+        $this->api_key = $api_key;
+        $this->api_url = $api_url;
+    }
+
+    /**
+     * Register a user.
+     *
+     * @param string $email        New user's email
+     * @param string $cellphone    New user's cellphone
+     * @param string $country_code New user's country code. defaults to USA(1)
+     *
+     * @return Authy_User the new registered user
+     */
+    public function registerUser($email, $cellphone, $country_code = 1)
+    {
+        $params = $this->defaultParams();
+        $params['user'] = array(
+            "email" => $email,
+            "country_code" => $country_code,
+            "cellphone" => $cellphone
+        );
+
+        $resp = $this->rest->post('/protected/json/users/new', $params);
+
+        return new Authy_User($resp);
+    }
+
+    /**
+     * Verify a given token.
+     *
+     * @param string $authy_id User's id stored in your database
+     * @param string $token    The token entered by the user
+     * @param string $opts     Array of options, for example: 
+     *                           array("force" => "true")
+     *
+     * @return Authy_Response the server response
+     */
+    public function verifyToken($authy_id, $token, $opts = array())
+    {
+        $params = array_merge($this->defaultParams(), $opts);
+        if (!array_key_exists("force", $params)) {
+            $params["force"] = "true";
+        }
+        $url = '/protected/json/verify/'. urlencode($token)
+                                        .'/'. urlencode($authy_id);
+        $resp = $this->rest->get($url, $params);
+
+        return new Authy_Response($resp);
+    }
+
+    /**
+     * Request a valid token via SMS.
+     *
+     * @param string $authy_id User's id stored in your database
+     * @param string $opts     Array of options, for example:
+     *                           array("force" => "true")
+     *
+     * @return Authy_Response the server response
+     */
+    public function requestSms($authy_id, $opts = array())
+    {
+        $params = array_merge($this->defaultParams(), $opts);
+        $url = '/protected/json/sms/'.urlencode($authy_id);
+
+        $resp = $this->rest->get($url, $params);
+
+        return new Authy_Response($resp);
+    }
+	
+    /**
+     * Cellphone call, usually used with SMS Token issues or if no smartphone is available.
+	 * This function needs the app to be on Starter Plan (free) or higher.
+     *
+     * @param string $authy_id User's id stored in your database
+     * @param string $opts     Array of options, for example:
+     *                           array("force" => "true")
+     *
+     * @return Authy_Response the server response
+     */
+    public function phoneCall($authy_id, $opts = array())
+    {
+        $params = array_merge($this->defaultParams(), $opts);
+        $url = '/protected/json/call/'.urlencode($authy_id);
+
+        $resp = $this->rest->get($url, $params);
+
+        return new Authy_Response($resp);
+    }
+	
+    /**
+     * Deletes an user.
+     *
+     * @param string $authy_id User's id stored in your database
+     *
+     * @return Authy_Response the server response
+     */	
+    public function deleteUser($authy_id)
+    {
+		$params = array_merge($this->defaultParams());
+        $url = '/protected/json/users/delete/'.urlencode($authy_id);
+
+        $resp = $this->rest->post($url, $params);
+
+        return new Authy_Response($resp);
+    }	
+
+    /**
+     * Return the default parameters.
+     *
+     * @return array array with the default parameters
+     */
+    protected function defaultParams()
+    {
+        return array("api_key" => $this->api_key);
+    }
+};

+ 85 - 0
lib/authy-php/lib/Authy/Response.php

@@ -0,0 +1,85 @@
+<?php
+
+/**
+ * ApiClient
+ *
+ * PHP version 5
+ *
+ * @category Services
+ * @package  Authy
+ * @author   David Cuadrado <[email protected]>
+ * @license  http://creativecommons.org/licenses/MIT/ MIT
+ * @link     http://authy.github.com/pear
+ */
+
+/**
+ * Friendly class to parse response from the authy API
+ *
+ * @category Services
+ * @package  Authy
+ * @author   David Cuadrado <[email protected]>
+ * @license  http://creativecommons.org/licenses/MIT/ MIT
+ * @link     http://authy.github.com/pear
+ */
+class Authy_Response
+{
+    protected $raw_response;
+    protected $body;
+    protected $errors;
+
+    /**
+     * Constructor.
+     *
+     * @param array $raw_response Raw server response
+     */
+    public function __construct($raw_response)
+    {
+        $this->raw_response = $raw_response;
+        $this->body = $raw_response['body'];
+        $this->errors = new stdClass();
+
+        // Handle errors
+        if (isset($this->body->errors)) {
+            $this->errors = $this->body->errors; // when response is {errors: {}}
+            unset($this->body->errors);
+        } else if ($raw_response['status'] == 400) {
+            $this->errors = $this->body; // body here is a stdClass
+            $this->body = new stdClass();
+        } else if (!$this->ok() && gettype($this->body) == 'string') {
+             // the response was an error so put the body as an error
+            $this->errors = (object) array("error" => $this->body);
+            $this->body = new stdClass();
+        }
+    }
+
+    /**
+     * Check if the response was ok
+     *
+     * @return boolean return true if the response code is 200
+     */
+    public function ok()
+    {
+        return $this->raw_response['status'] == 200;
+    }
+
+    /**
+     * Returns the id of the response if present
+     *
+     * @return integer id of the response
+     */
+    public function id()
+    {
+        return isset($this->body->id) ? $this->body->id : null;
+    }
+
+
+    /**
+     * Get the request errors
+     *
+     * @return stdClass object containing the request errors
+     */
+    public function errors()
+    {
+        return $this->errors;
+    }
+}

+ 41 - 0
lib/authy-php/lib/Authy/User.php

@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * ApiClient
+ *
+ * PHP version 5
+ *
+ * @category Services
+ * @package  Authy
+ * @author   David Cuadrado <[email protected]>
+ * @license  http://creativecommons.org/licenses/MIT/ MIT
+ * @link     http://authy.github.com/pear
+ */
+
+/**
+ * User implementation. Extends from Authy_Response
+ *
+ * @category Services
+ * @package  Authy
+ * @author   David Cuadrado <[email protected]>
+ * @license  http://creativecommons.org/licenses/MIT/ MIT
+ * @link     http://authy.github.com/pear
+ */
+class Authy_User extends \Authy_Response
+{
+
+    /**
+     * Constructor.
+     *
+     * @param array $raw_response Raw server response
+     */
+    public function __construct($raw_response)
+    {
+        if (isset($raw_response['body']->user)) {
+            // response is {user: {id: id}}
+            $raw_response['body'] = $raw_response['body']->user;
+        }
+
+        parent::__construct($raw_response);
+    }
+}

+ 846 - 0
lib/authy-php/lib/vendor/Resty.php

@@ -0,0 +1,846 @@
+<?php
+/**
+ * A simple PHP library for doing RESTful HTTP stuff. Does *not* require the curl extension.
+ * @link https://github.com/fictivekin/resty.php
+ */
+class Resty
+{
+
+	/**
+	 * The version of this lib
+	 */
+	const VERSION = '0.3.8';
+
+	const DEFAULT_TIMEOUT = 240;
+
+	/**
+	 * @var bool enables debugging output
+	 */
+	protected $debug = false;
+
+	/**
+	 * logging function (should be a Closure)
+	 * @var Closure
+	 */
+	protected $logger = null;
+
+	/**
+	 * @var bool whether or not to auto-parse the response body as JSON or XML
+	 */
+	protected $parse_body = true;
+
+	/**
+	 * @var string
+	 * @see Resty::getUserAgent()
+	 */
+	protected $user_agent = null;
+
+	/**
+	 * @var string
+	 */
+	protected $base_url;
+
+	/**
+	 * Stores the last request hash
+	 * @var array
+	 */
+	protected $last_request;
+
+	/**
+	 * Stores the last response hash
+	 * @var array
+	 */
+	protected $last_response;
+
+	/**
+	 * stores anon func callbacks (because you can't store them as obj props
+	 * @var array
+	 */
+	protected $callbacks = array();
+
+	/**
+	 * username for basic auth
+	 * @var string
+	 */
+	protected $username;
+
+	/**
+	 * password for basic auth
+	 * @var string
+	 */
+	protected $password;
+
+	/**
+	 * by default, silence the fopen warning if we can't open the stream
+	 */
+	protected $silence_fopen_warning = true;
+
+	/**
+	 * by default, don't raise an exception if fopen() fails
+	 * @var boolean
+	 */
+	protected $raise_fopen_exception = false;
+
+
+	/**
+	 * content-types that will trigger JSON parsing of body
+	 * @var array
+	 */
+	public static $JSON_TYPES = array(
+		'application/json',
+		'text/json',
+		'text/x-json',
+	);
+
+	/**
+	 * content-types that will trigger XML parsing
+	 * @var array
+	 */
+	public static $XML_TYPES = array(
+		'application/xml',
+		'text/xml',
+		'application/rss+xml',
+		'application/xhtml+xml',
+		'application/atom+xml',
+		'application/xslt+xml',
+		'application/mathml+xml',
+	);
+
+
+	/**
+	 * Passed opts can include
+	 * $opts['onRequestLog'] - an anonymous function that takes the Resty::last_request property as arg
+	 * $opts['onResponseLog'] - an anonymous function that takes the Resty::last_response property as arg
+	 *
+	 * @see Resty::last_request
+	 * @see Resty::last_response
+	 * @see Resty::sendRequest()
+	 * @param array $opts OPTIONAL array of options
+	 */
+	function __construct($opts=null) {
+		if (!empty($opts['onRequestLog']) && ($opts['onRequestLog'] instanceof Closure)) {
+			$this->callbacks['onRequestLog'] = $opts['onRequestLog'];
+		}
+		if (!empty($opts['onResponseLog']) && ($opts['onResponseLog'] instanceof Closure)) {
+			$this->callbacks['onResponseLog'] = $opts['onResponseLog'];
+		}
+		if (isset($opts['silence_fopen_warning'])) {
+			$this->silenceFopenWarning((bool)$opts['silence_fopen_warning']);
+		}
+		if (isset($opts['raise_fopen_exception'])) {
+			$this->raiseFopenException((bool)$opts['raise_fopen_exception']);
+		}
+	}
+
+
+	/**
+	 * retrieve the last request we sent
+	 *
+	 * valid keys are ['url', 'method', 'querydata', 'headers', 'options', 'opts']
+	 *
+	 * @param string $key just retrieve a given field from the hash
+	 * @return mixed
+	 */
+	public function getLastRequest($key=null) {
+		if (!isset($key)) {
+			return $this->last_request;
+		}
+
+		return $this->last_request[$key];
+
+	}
+
+	/**
+	 * retrieve the last response we got
+	 *
+	 * valid keys are ['meta', 'status', 'headers', 'body']
+	 *
+	 * @param string $key just retrieve a given field from the hash
+	 * @return mixed
+	 */
+	public function getLastResponse($key=null) {
+		if (!isset($key)) {
+			return $this->last_response;
+		}
+
+		return $this->last_response[$key];
+
+	}
+
+	/**
+	 * make a GET request
+	 *
+	 * @param string the URL. This will be appended to the base_url, if any set
+	 * @param array $querydata hash of key/val pairs
+	 * @param array $headers hash of key/val pairs
+	 * @param array $options hash of key/val pairs ('timeout')
+	 * @return array the response hash
+	 * @see Resty::sendRequest()
+	 */
+	public function get($url, $querydata=null, $headers=null, $options=null) {
+		return $this->sendRequest($url, 'GET', $querydata, $headers, $options);
+	}
+
+	/**
+	 * make a POST request
+	 *
+	 * @param string the URL. This will be appended to the base_url, if any set
+	 * @param array $querydata hash of key/val pairs
+	 * @param array $headers hash of key/val pairs
+	 * @param array $options hash of key/val pairs ('timeout')
+	 * @return array the response hash
+	 * @see Resty::sendRequest()
+	 */
+	public function post($url, $querydata=null, $headers=null, $options=null) {
+		return $this->sendRequest($url, 'POST', $querydata, $headers, $options);
+	}
+
+	/**
+	 * make a PUT request
+	 *
+	 * @param string the URL. This will be appended to the base_url, if any set
+	 * @param array $querydata hash of key/val pairs
+	 * @param array $headers hash of key/val pairs
+	 * @param array $options hash of key/val pairs ('timeout')
+	 * @return array the response hash
+	 * @see Resty::sendRequest()
+	 */
+	public function put($url, $querydata=null, $headers=null, $options=null) {
+		return $this->sendRequest($url, 'PUT', $querydata, $headers, $options);
+	}
+
+	/**
+	 * make a DELETE request
+	 *
+	 * @param string the URL. This will be appended to the base_url, if any set
+	 * @param array $querydata hash of key/val pairs
+	 * @param array $headers hash of key/val pairs
+	 * @param array $options hash of key/val pairs ('timeout')
+	 * @return array the response hash
+	 * @see Resty::sendRequest()
+	 */
+	public function delete($url, $querydata=null, $headers=null, $options=null) {
+		return $this->sendRequest($url, 'DELETE', $querydata, $headers, $options);
+	}
+
+	/**
+	 * @param string $url
+	 * @param array  $files
+	 * @param array  $params
+	 * @param array  $headers
+	 * @param array  $options
+	 *
+	 * The $files array should be a set of key/val pairs, with the key being
+	 * the field name, and the val the file path. ex:
+	 * $files['avatar'] = '/path/to/file.jpg';
+	 * $files['background'] = '/path/to/file2.jpg';
+	 *
+	 */
+	public function postFiles($url, $files, $params=null, $headers=null, $options=null) {
+
+		$datastr = "";
+		$boundary = "---------------------".substr(md5(rand(0,32000)), 0, 10);
+
+		// build params
+		if (isset($params)) {
+			foreach($params as $key => $val) {
+				$datastr .= "--$boundary\n";
+				$datastr .= "Content-Disposition: form-data; name=\"".$key."\"\n\n".$val."\n";
+			}
+		}
+		$datastr .= "--$boundary\n";
+
+		// build files
+		foreach($files as $key => $file)
+		{
+			$filename = pathinfo($file, PATHINFO_BASENAME);
+			$content_type = $this->getMimeType($file);
+			$fileContents = file_get_contents($file);
+
+			$datastr .= "Content-Disposition: form-data; name=\"{$key}\"; filename=\"{$filename}\"\n";
+			$datastr .= "Content-Type: {$content_type}\n";
+			$datastr .= "Content-Transfer-Encoding: binary\n\n";
+			$datastr .= $fileContents."\n";
+			$datastr .= "--$boundary\n";
+		}
+
+		if (!isset($headers)) {
+			$headers = array();
+		}
+		$headers['Content-Type'] = 'multipart/form-data; boundary='.$boundary;
+
+		return $this->post($url, $datastr, $headers, $options);
+	}
+
+
+	/**
+	 * @param string $url
+	 * @param array  $binary_data
+	 * @param array  $params
+	 * @param array  $headers
+	 * @param array  $options
+	 *
+	 * The $binary_data array should be a set of key/val pairs, with the key being
+	 * the field name, and the val the binary data. ex:
+	 * $files['avatar'] = <BINARY>;
+	 * $files['background'] = <BINARY>;
+	 *
+	 * with that data, a multipart POST body is created, identical to a file
+	 * upload, just without reading the data from a file
+	 *
+	 */
+	public function postBinary($url, $binary_data, $params=null, $headers=null, $options=null) {
+
+		$datastr = "";
+		$boundary = "---------------------".substr(md5(rand(0,32000)), 0, 10);
+
+		// build params
+		if (isset($params)) {
+			foreach($params as $key => $val) {
+				$datastr .= "--$boundary\n";
+				$datastr .= "Content-Disposition: form-data; name=\"".$key."\"\n\n".$val."\n";
+			}
+		}
+		$datastr .= "--$boundary\n";
+
+		// build files
+		foreach($binary_data as $key => $bdata)
+		{
+			$filename = 'bdata';
+			$content_type = 'application/octet-stream';
+
+			$datastr .= "Content-Disposition: form-data; name=\"{$key}\"; filename=\"{$filename}\"\n";
+			$datastr .= "Content-Type: {$content_type}\n";
+			$datastr .= "Content-Transfer-Encoding: binary\n\n";
+			$datastr .= $bdata."\n";
+			$datastr .= "--$boundary\n";
+		}
+
+		if (!isset($headers)) {
+			$headers = array();
+		}
+		$headers['Content-Type'] = 'multipart/form-data; boundary='.$boundary;
+
+		return $this->post($url, $datastr, $headers, $options);
+	}
+
+	/**
+	 * @see Resty::postFiles()
+	 *
+	 * Stole this from the Amazon S3 class:
+	 *
+	 * Copyright (c) 2008, Donovan Schönknecht.  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.
+	 *
+	 * 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.
+	 *
+	 * Amazon S3 is a trademark of Amazon.com, Inc. or its affiliates.
+	 *
+	 */
+	protected function getMimeType($filepath) {
+
+		if (extension_loaded('fileinfo') && isset($_ENV['MAGIC']) &&
+			($finfo = finfo_open(FILEINFO_MIME, $_ENV['MAGIC'])) !== false) {
+
+			if (($type = finfo_file($finfo, $filepath)) !== false) {
+				// Remove the charset and grab the last content-type
+				$type = explode(' ', str_replace('; charset=', ';charset=', $type));
+				$type = array_pop($type);
+				$type = explode(';', $type);
+				$type = trim(array_shift($type));
+			}
+			finfo_close($finfo);
+
+		// If anyone is still using mime_content_type()
+		} elseif (function_exists('mime_content_type')) {
+			$type = trim(mime_content_type($filepath));
+		}
+
+		if ($type !== false && strlen($type) > 0) { return $type; }
+
+		// Otherwise do it the old fashioned way
+		static $exts = array(
+			'jpg' => 'image/jpeg', 'gif' => 'image/gif', 'png' => 'image/png',
+			'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'ico' => 'image/x-icon',
+			'swf' => 'application/x-shockwave-flash', 'pdf' => 'application/pdf',
+			'zip' => 'application/zip', 'gz' => 'application/x-gzip',
+			'tar' => 'application/x-tar', 'bz' => 'application/x-bzip',
+			'bz2' => 'application/x-bzip2', 'txt' => 'text/plain',
+			'asc' => 'text/plain', 'htm' => 'text/html', 'html' => 'text/html',
+			'css' => 'text/css', 'js' => 'text/javascript',
+			'xml' => 'text/xml', 'xsl' => 'application/xsl+xml',
+			'ogg' => 'application/ogg', 'mp3' => 'audio/mpeg', 'wav' => 'audio/x-wav',
+			'avi' => 'video/x-msvideo', 'mpg' => 'video/mpeg', 'mpeg' => 'video/mpeg',
+			'mov' => 'video/quicktime', 'flv' => 'video/x-flv', 'php' => 'text/x-php'
+		);
+		$ext = strtolower(pathInfo($filepath, PATHINFO_EXTENSION));
+
+		return isset($exts[$ext]) ? $exts[$ext] : 'application/octet-stream';
+	}
+
+	/**
+	 * bc wrapper
+	 */
+	public function enableDebugging($state=false) {
+		$this->debug($state);
+	}
+
+	/**
+	 * enable or disable debugging. If no arg passed, just returns current state
+	 * @param bool $state=null if not passed, state not changed
+	 * @return boolean the current state
+	 */
+	public function debug($state=null) {
+		if (isset($state)) {
+			$this->debug = (bool)$state;
+		}
+		return $this->debug;
+	}
+
+	/**
+	 * raise an exception from fopen if trying to open stream fails
+	 * @param  boolean $state=null optional, set the state
+	 * @return boolean the current state
+	 */
+	public function raiseFopenException($state=null) {
+		if (isset($state)) {
+			$this->raise_fopen_exception = (bool)$state;
+		}
+		return $this->raise_fopen_exception;
+	}
+
+	/**
+	 * silence warnings from fopen when trying to open stream
+	 * @param  boolean $state=null optional, set the state
+	 * @return boolean the current state
+	 */
+	public function silenceFopenWarning($state=null) {
+		if (isset($state)) {
+			$this->silence_fopen_warning = (bool)$state;
+		}
+		return $this->silence_fopen_warning;
+	}
+
+	/**
+	 * sets an alternate logging method
+	 * @param Closure $logger
+	 */
+	public function setLogger(Closure $logger) {
+		$this->logger = $logger;
+	}
+
+	/**
+	 * enable or disable automatic parsing of body. default is true
+	 * @param bool $state default TRUE
+	 */
+	public function parseBody($state=true) {
+		$state = (bool)$state;
+		$this->parse_body = $state;
+	}
+
+	/**
+	 * Sets the base URL for all subsequent requests
+	 * @param string $base_url
+	 */
+	public function setBaseURL($base_url) {
+		$this->base_url = $base_url;
+	}
+
+	/**
+	 * retrieves the current Resty::$base_url
+	 * @return string
+	 */
+	public function getBaseURL() {
+		return $this->base_url;
+	}
+
+	/**
+	 * Sets the user-agent
+	 * @param string $user_agent
+	 */
+	public function setUserAgent($user_agent) {
+		$this->user_agent = $user_agent;
+	}
+
+	/**
+	 * Gets the current user agent. if Resty::$user_agent is not set, uses a default
+	 * @return string
+	 */
+	public function getUserAgent() {
+		if (empty($this->user_agent)) {
+			$this->user_agent = 'Resty ' . static::VERSION;
+		}
+		return $this->user_agent;
+	}
+
+	/**
+	 * Sets credentials for http basic auth
+	 * @param string $username
+	 * @param string $password
+	 */
+	public function setCredentials($username, $password) {
+		$this->username = $username;
+		$this->password = $password;
+	}
+
+	/**
+	 * removes current credentials
+	 */
+	public function clearCredentials() {
+		$this->username = null;
+		$this->password = null;
+	}
+
+	/**
+	 * takes a set of key/val pairs and builds an array of raw header strings
+	 *
+	 * @param string $headers
+	 * @return void
+	 * @author Ed Finkler
+	 */
+	protected function buildHeadersArray($headers) {
+		$str_headers = array();
+		foreach ($headers as $key => $value) {
+			$str_headers[] = "{$key}: {$value}";
+		}
+		return $str_headers;
+	}
+
+	/**
+	 * Extracts the headers of a response from the stream's meta data
+	 * @param array $meta
+	 * @return array
+	 */
+	protected function metaToHeaders($meta) {
+		$headers = array();
+
+		if (!isset($meta['wrapper_data'])) {
+			return $headers;
+		}
+
+		foreach ($meta['wrapper_data'] as $value) {
+			if (strpos($value, 'HTTP') !== 0) {
+				preg_match("|^([^:]+):\s?(.+)$|", $value, $matches);
+				if (is_array($matches) && isset($matches[2])) {
+					$headers[trim($matches[1])] = trim($matches[2], " \t\n\r\0\x0B\"");
+				}
+			}
+		}
+		return $headers;
+	}
+
+	/**
+	 * extracts the status code from the stream meta data
+	 * @param array $meta
+	 * @return integer
+	 */
+	protected function getStatusCode($meta) {
+		$matches = array();
+		$status = 0;
+		preg_match("|\s(\d\d\d)\s?|", $meta['wrapper_data'][0], $matches);
+		if (is_array($matches) && isset($matches[1])) {
+			$status = (int)trim($matches[1]);
+		}
+		return $status;
+	}
+
+	/**
+	 * Sends the HTTP request and retrieves/parses the response
+	 *
+	 * @param string $url
+	 * @param string $method
+	 * @param array $querydata OPTIONAL
+	 * @param array $headers OPTIONAL
+	 * @param array $options OPTIONAL
+	 * @return array
+	 * @author Ed Finkler
+	 */
+	public function sendRequest($url, $method='GET', $querydata=null, $headers=null, $options=null) {
+		$resp = array();
+
+		if ($this->base_url) {
+			$url = $this->base_url.$url;
+		}
+
+		// we need to supply a default content-type
+		if (!isset($headers['Content-Type'])) {
+			$headers['Content-Type'] = 'application/x-www-form-urlencoded';
+		}
+
+		// by default, pass the header "Connection: close"
+		if (!isset($headers['Connection'])) {
+			$headers['Connection'] = 'close';
+		}
+
+		// if we have a username and password, use it
+		if (isset($this->username) && isset($this->password) && !isset($headers['Authorization'])) {
+			$this->log("{$this->username}:{$this->password}");
+			$headers['Authorization'] = 'Basic '.base64_encode("{$this->username}:{$this->password}");
+		}
+
+		// default timeout
+		$timeout = isset($options['timeout']) ? $options['timeout'] : static::DEFAULT_TIMEOUT;
+
+		$content = null;
+
+		// if querydata is a string, just pass it as-is
+		if (isset($querydata) && is_string($querydata)) {
+
+			$content = $querydata;
+
+		// else if it's an array, make an http query
+		} elseif (isset($querydata) && is_array($querydata)) {
+
+			$content = http_build_query($querydata);
+
+		}
+
+		// create an array of header strings from the hash
+		$headerarr = isset($headers) ? $this->buildHeadersArray($headers) : array();
+
+		// GET and DELETE should use the URL to pass data
+		$urlcontent = ('GET' === $method || 'DELETE' === $method);
+
+		// if this is a GET or DELETE and we have some $content, append to URL
+		if ($urlcontent && isset($content)) {
+			$url .= '?'.$content;
+		}
+
+
+		$opts = array(
+			'http'=>array(
+				'timeout'=>$timeout,
+				'method'=>$method,
+				'content'=> (!$urlcontent) ? $content : null,
+				'user_agent'=>$this->getUserAgent(),
+				'header'=>$headerarr,
+				'ignore_errors'=>1
+			)
+		);
+
+		$this->log('URL =================');
+		$this->log($url);
+
+		$this->log('METHOD =================');
+		$this->log($method);
+
+		$this->log('QUERYDATA =================');
+		$this->log($querydata);
+
+		$this->log('HEADERS =================');
+		$this->log($headers);
+
+		$this->log('OPTIONS =================');
+		$this->log($options);
+
+		$this->log('OPTS =================');
+		$this->log($opts);
+
+		$this->last_request = compact('url', 'method', 'querydata', 'headers', 'options', 'opts');
+		// call custom req log callback
+		if (!empty($this->callbacks['onRequestLog'])) {
+			$this->callbacks['onRequestLog']($this->last_request);
+		}
+
+
+		$resp_data = $this->makeStreamRequest($url, $opts);
+
+		$resp['meta'] = $resp_data['meta'];
+		$resp['body'] = $resp_data['body'];
+		$resp['error'] = $resp_data['error'];
+		$resp['error_msg'] = $resp_data['error_msg'];
+		$resp['status'] = $this->getStatusCode($resp['meta']);
+		$resp['headers'] = $this->metaToHeaders($resp['meta']);
+		$this->log($resp);
+
+		$this->log("Processing response body…");
+		$resp = $this->processResponseBody($resp);
+		$this->log($resp['body']);
+
+		$this->last_response = $resp;
+
+		// call custom resp log callback
+		if (!empty($this->callbacks['onResponseLog'])) {
+			$this->callbacks['onResponseLog']($this->last_response);
+		}
+
+		return $resp;
+	}
+
+
+	/**
+	 * opens an http stream, sends the request, and returns result
+	 * @param  [type] $url  [description]
+	 * @param  [type] $opts [description]
+	 * @return [type]       [description]
+	 */
+	protected function makeStreamRequest($url, $opts) {
+
+		$resp_data = array(
+			'meta' => null,
+			'body' => null,
+			'error' => true,
+			'error_msg' => null,
+		);
+
+		$context = stream_context_create($opts);
+
+		$this->log("Sending…");
+		$start_time = microtime(true);
+
+		$this->log("Opening stream…");
+		if ($this->silence_fopen_warning) {
+			$stream = @fopen($url, 'r', false, $context);
+		} else {
+			$stream = fopen($url, 'r', false, $context);
+		}
+
+		if (!$stream) {
+
+			$req_time = static::calc_time_passed($start_time);
+			$opts_json = !empty($opts) ? json_encode($opts) : 'null';
+			$msg = "Stream open failed for '{$url}'; req_time: {$req_time}; opts: {$opts_json}";
+			$this->log($msg);
+
+			if ($this->raise_fopen_exception) {
+				throw new Exception($msg);
+			} else {
+				$resp_data['error'] = true;
+				$resp_data['error_msg'] = $msg;
+			}
+
+		} else {
+
+			$this->log("Getting metadata…");
+			$resp_data['meta'] = stream_get_meta_data($stream);
+
+			$this->log("Getting response…");
+			$resp_data['body'] = stream_get_contents($stream);
+
+			$this->log("Closing stream…");
+			fclose($stream);
+
+		}
+
+
+		if ($this->debug) {
+			$req_time = static::calc_time_passed($start_time);
+			$this->log(sprintf("Request time for \"%s %s\": %f", $opts['http']['method'], $url, $req_time));
+		}
+
+		return $resp_data;
+
+	}
+
+
+	/**
+	 * If we get back something that claims to be XML or JSON, parse it as such and assign to $resp['body']
+	 *
+	 * @param string $resp
+	 * @return string|object
+	 * @see Resty::$JSON_TYPES
+	 * @see Resty::$XML_TYPES
+	 */
+	protected function processResponseBody($resp) {
+
+		if ($this->parse_body === true) {
+
+			$header_content_type = isset($resp['headers']['Content-Type']) ? $resp['headers']['Content-Type'] : null;
+			$content_type = preg_split('/[;\s]+/', $header_content_type);
+			$content_type = $content_type[0];
+
+			if (in_array($content_type, static::$JSON_TYPES)) {
+
+				$this->log("Response body is JSON");
+				$resp['body_raw'] = $resp['body'];
+				$resp['body'] = json_decode($resp['body']);
+				return $resp;
+
+			} elseif (in_array($content_type, static::$XML_TYPES)) {
+
+				$this->log("Response body is XML");
+				$resp['body_raw'] = $resp['body'];
+				$resp['body'] = new \SimpleXMLElement($resp['body']);
+				return $resp;
+
+			}
+
+		}
+
+		$this->log("Response body not parsed");
+
+		return $resp;
+
+	}
+
+	/**
+	 * calculate time passed in microtime
+	 * @param  float $start_time should be result of microtime(true)
+	 * @return float the diff between passed microtime and current microtime
+	 */
+	protected static function calc_time_passed($start_time) {
+		$stop_time = microtime(true);
+		$req_time = $stop_time - $start_time;
+		return $req_time;
+	}
+
+
+	protected function log($msg) {
+		if (!$this->debug) { return; }
+
+		if (is_callable($this->logger)) {
+			$logger = $this->logger;
+			return $logger($msg);
+		}
+
+		return $this->default_logger($msg);
+	}
+
+
+	/**
+	 * logging helper
+	 *
+	 * @param mixed $msg
+	 */
+	protected function default_logger($msg) {
+
+		$line = date(\DateTime::RFC822) . " :: ";
+
+		if (is_string($msg)) {
+			$line .= "{$msg}\n";
+		} else {
+			ob_start();
+			var_dump($msg);
+			$line = ob_get_clean();
+			$line .= "\n";
+		}
+
+		if (PHP_SAPI !== 'cli') {
+			$line = "<pre>$line</pre>\n";
+		}
+
+		return error_log($line);
+	}
+
+
+
+}
+

+ 135 - 0
lib/configuration.php

@@ -0,0 +1,135 @@
+<?php
+	require_once(dirname(dirname(__FILE__))."/header.php");
+	function get_conf($key){
+		$res = query("SELECT c.value FROM configuration c WHERE c.key = '%s'",array($key));
+		if(!$res || $res->num_rows != 1 ){
+			return false;
+		}
+		$res = $res->fetch_assoc();
+		return $res['value'];
+	}
+	function set_conf($key,$val){
+		return query("UPDATE configuration c SET c.value = '%s' WHERE c.key = '%s'",array($val,$key));
+	}
+	function get_conf_type($key){
+		$res = query("SELECT c.type FROM configuration c WHERE c.key = '%s'",array($key));
+		if($res && $row = $res->fetch_assoc()){
+			return $row['type'];
+		}
+		return 'string';
+	}
+	function get_conf_values($key,$labels=false){
+		$ret = array();
+		switch(get_conf_type($key)){
+			case 'list':
+				$res = query("SELECT cl.value,cl.label FROM configuration_lists cl WHERE cl.key='%s'",array($key));
+				if($res){
+					while($row = $res->fetch_assoc()){
+						if(!$labels){
+							$row = $row['value'];
+						}
+						array_push($ret,$row);
+					}
+				}else{
+					array_push($ret,get_conf($key));
+				}
+			break;
+			case 'lookup':
+				$res = query("SELECT cl.table,cl.column,cl.label_column,cl.enabled_column FROM configuration_lookups cl WHERE cl.key='%s'",array($key));
+				if($res && $res->num_rows == 1){
+					$lookup = $res->fetch_assoc();
+					if(isset($lookup['enabled_column']) && !is_null($lookup['enabled_column']) && $lookup['enabled_column'] != ''){
+						$eq = 1;
+						if(substr($lookup['enabled_column'],0,1) == '!'){
+							$eq = 0;
+							$lookup['enabled_column'] = substr($lookup['enabled_column'],1);
+						}
+						$res = query("SELECT t.%s AS value, t.%s AS label FROM %s t WHERE t.%s = {$eq}",array($lookup['column'],$lookup['label_column'],$lookup['table'],$lookup['enabled_column']));
+					}else{
+						$res = query("SELECT t.%s AS value, t.%s AS label FROM %s t",array($lookup['column'],$lookup['label_column'],$lookup['table']));
+					}
+					if($res){
+						while($row = $res->fetch_assoc()){
+							if(!$labels){
+								$row = $row['value'];
+							}
+							array_push($ret,$row);
+						}
+					}else{
+						array_push($ret,get_conf($key));
+					}
+				}else{
+					array_push($ret,get_conf($key));
+				}
+			break;
+		}
+		return $ret;
+	}
+	function get_conf_list(){
+		$conf = array();
+		$res = query("SELECT c.key,c.description,c.value,c.type FROM configuration c");
+		if($res){
+			while($row = $res->fetch_assoc()){
+				$item = array(
+					'key'=>$row['key'],
+					'type'=>get_conf_type($row['key']),
+					'label'=>isset($row['description'])?$row['description']:''
+				);
+				$item['value'] = $row['value'];
+				if(!isset($item['value'])){
+					$item['value'] = '';
+				}
+				if(isset($item['type'])){
+					switch($item['type']){
+						case 'list':case 'lookup':
+							$item['type'] = 'select';
+							$values = get_conf_values($item['key'],true);
+							$item['values'] = array();
+							foreach($values as $value){
+								if(isset($item['value']) && $value['value'] == $item['value']){
+									$value['attributes'] = array(
+										'selected'=>'selected'
+									);
+								}
+								array_push($item['values'],$value);
+							}
+						break;
+					}
+					array_push($conf,$item);
+				}
+			}
+		}
+		return $conf;
+	}
+	function render_configuration_table(){
+		$items = array(
+			array(
+				'name'=>'action',
+				'type'=>'hidden',
+				'value'=>'config'
+			)
+		);
+		$config = get_conf_list();
+		foreach($config as $k => $conf){
+			switch($conf['type']){
+				case 'select':
+					$item = array(
+						'name'=>$conf['key'],
+						'values'=>$conf['values'],
+						'label'=>$conf['label'],
+						'type'=>'select'
+					);
+				break;
+				default:
+					$item = array(
+						'name'=>$conf['key'],
+						'value'=>$conf['value'],
+						'label'=>$conf['label'],
+						'type'=>$conf['type']
+					);
+			}
+			array_push($items,$item);
+		}
+		return get_form_html('configuration',$items,'Save');
+	}
+?>

+ 74 - 0
lib/forms.php

@@ -0,0 +1,74 @@
+<?php
+	require_once(dirname(dirname(__FILE__))."/header.php");
+	function get_form_html($id,$fields,$sublabel){
+		array_push($fields,Array(
+			'type'=>'submit',
+			'value'=>$sublabel
+		));
+		return get_form_html_advanced(Array(
+			'id'=>$id
+		),$fields);
+	}
+	function get_form_html_advanced($attributes,$fields){
+		$r = "<form";
+		foreach($attributes as $attribute => $value){
+			$r .= " {$attribute}=\"{$value}\"";
+		}
+		$r.= ">\n";
+		foreach($fields as $k => $field){
+			$r .= get_field_html($field);
+		}
+		return $r."</form>\n";
+	}
+	function get_field_html($field){
+		$a = '';
+		if(isset($field['attributes'])){
+			foreach($field['attributes'] as $attribute => $value){
+				$a .= " {$attribute}=\"{$value}\"";
+			}
+		}
+		$v = '';
+		if(isset($field['value'])&&!is_null($field['value'])&&$field['value']!=''){
+			$v = "value='{$field['value']}'";
+		}
+		switch($field['type']){
+			case 'select':
+				$r = "<div class='row'><label for='{$field['name']}'>{$field['label']}</label><span><select name='{$field['name']}'{$a}>";
+				foreach($field['values'] as $k => $opt){
+					$a = '';
+					if(isset($opt['attributes']) && is_array($opt['attributes'])){
+						foreach($opt['attributes'] as $attribute => $value){
+							$a .= " {$attribute}=\"{$value}\"";
+						}
+					}
+					if(isset($field['value'])&&$field['value']==$opt['value']){
+						$a .= "selected=\"selected\"";
+					}
+					$r .= "<option value='{$opt['value']}'{$a}>{$opt['label']}</option>";
+				}
+				$r .= "</select></span></div>";
+			break;
+			case 'hidden':
+				$r = "<input type='hidden' name='{$field['name']}'{$v}{$a}/>";
+			break;
+			case 'custom':
+				$r = $field['html'];
+			break;
+			case 'section':
+				$r = "<div class='form_section'{$a}>";
+				if(isset($field['fields'])){
+					foreach($field['fields'] as $k => $subfield){
+						$r .= get_field_html($subfield);
+					}
+				}
+				$r .= "</div>";
+			break;
+			case 'submit':
+				$r = "<input type='submit' {$v}{$a}/>";
+			break;
+			case 'text':default:
+				$r = "<div class='row'><label for='{$field['name']}'>{$field['label']}</label><span><input type='{$field['type']}' name='{$field['name']}'{$v}{$a}/></span></div>";
+		}
+		return $r."\n";
+	}
+?>

+ 207 - 0
lib/irc.php

@@ -0,0 +1,207 @@
+<?php
+	require_once(dirname(dirname(__FILE__))."/header.php");
+	require_once('xmlrpc.php');
+	function atheme_login($hostname,$port, $path, $username, $password){
+		$message = new xmlrpcmsg("atheme.login");
+		$message->addParam(new xmlrpcval($username, "string"));
+		$message->addParam(new xmlrpcval($password, "string"));
+		$client = new xmlrpc_client($path, $hostname, $port);
+		$response = $client->send($message);
+		if(!$response->faultCode()){
+			$session = explode("<string>", $response->serialize());
+			$session = explode("</string", $session[1]);
+			$session = $session[0];
+			return Array(true,$session);
+		}else{
+			return Array(
+				false,
+				'['.$response->faultCode().'] '.$response->faultString()
+			);
+		}
+	}
+	function atheme_command($hostname, $port, $path, $sourceip, $username, $password, $service, $command, $params=NULL){
+		$message = new xmlrpcmsg("atheme.login");
+		$message->addParam(new xmlrpcval($username, "string"));
+		$message->addParam(new xmlrpcval($password, "string"));
+		$client = new xmlrpc_client($path, $hostname, $port);
+		$response = $client->send($message);
+
+		$session = NULL;
+		if(!$response->faultCode()){
+			$session = explode("<string>", $response->serialize());
+			$session = explode("</string", $session[1]);
+			$session = $session[0];
+		}else{
+			switch($response->faultCode()){
+				case 1:
+					$m = 'Insufficient Parameters to login';
+				break;
+				case 3:
+					$m = "Account is not registered";
+				break;
+				case 5:
+					$m = "Invalid Username/Password";
+				break;
+				case 6:
+					$m = "Account is frozen";
+				break;
+				default:
+					$m = "Could not log in";
+			}
+			return Array(false,$m);
+		}
+		$message = new xmlrpcmsg("atheme.command");
+		$message->addParam(new xmlrpcval($session, "string"));
+		$message->addParam(new xmlrpcval($username, "string"));
+		$message->addParam(new xmlrpcval($sourceip, "string"));
+		$message->addParam(new xmlrpcval($service, "string"));
+		$message->addParam(new xmlrpcval($command, "string"));
+		if($params != NULL){
+			if(sizeof($params) < 2){
+				foreach($params as $param){
+					$message->addParam(new xmlrpcval($param, "string"));
+				}
+			}else{
+				$firstparam = $params[0];
+				$secondparam = "";
+				for($i = 1; $i < sizeof($params); $i++){
+					$secondparam .= $params[$i] . " ";
+				}
+				$secondparam = rtrim($secondparam);
+				$message->addParam(new xmlrpcval($firstparam, "string"));
+				$message->addParam(new xmlrpcval($secondparam, "string"));
+			}
+		}
+		$response = $client->send($message);
+		if(!$response->faultCode()){
+			$response = explode("<string>", $response->serialize());
+			$response = explode("</string", $response[1]);
+			$response = $response[0];
+			return Array(true,$response);
+		}else{
+			return Array(false,"Command failed: " . $response->faultString());
+		}
+	}
+	$ircret = "";
+	function ircputs($line){
+		global $msg;
+		global $irc;
+		$msg .= str_replace(get_conf('rehash-pass','string'),'**********',$line);
+		try{
+			error_reporting(0);
+			$r = fputs($irc,$line);
+			error_reporting(E_ALL);
+		}catch(Exception $e){
+			$r = false;
+			ircclose($e->code,$e->message);
+		}
+		return $r;
+	}
+	function ircclose($code=0,$message=null){
+		global $msg;
+		global $irc;
+		global $ircret;
+		try{
+			error_reporting(0);
+			$msg .= 'QUIT :'.$message;
+			fputs($irc,'QUIT :'.$message);
+			error_reporting(E_ALL);
+		}catch(Exception $e){}
+		while(!feof($irc) && $line = fgets($irc,128)){
+			if(is_string($line)){
+				$msg .= $line;
+			}
+		}
+		fclose($irc);
+		$ircret = '{"code":'.$code.',"message":"'.$message.'","log":'.json_encode($msg).'}';
+		return $ircret;
+	}
+	function isval($src,$prop,$val){
+		return isset($src[$prop]) && $src[$prop] == $val;
+	}
+	function ircrehash(){
+		global $msg;
+		global $irc;
+		global $ircret;
+		global $u;
+		global $user;
+		if(!isset($u)){
+			$u = $user;
+		}
+		$msg = '';
+		if(!$irc = fsockopen(get_conf('irc-server'),get_conf('irc-port'))){return ircclose(1,"Could not connect.");}
+		stream_set_timeout($irc,1) or ircclose(2,"Could not set timeout.");
+		while(!feof($irc)&&!$msg = fgets($irc,128)){}
+		if(!ircputs("NICK RehashServ\r\n")){return $ircret;}
+		if(!ircputs("USER RehashServ omni.irc.omnimaga.org RehashServ :RehashServ\r\n")){return $ircret;}
+		while(!feof($irc)){
+			$line = fgets($irc,128);
+			if(is_string($line)){
+				$msg .= $line;
+				$data = explode(' ',$line);
+				if(isval($data,1,'433')){
+					return ircclose(4,"RehashServ is already running.");
+				}elseif(strrpos($line,'ERROR :Closing Link:') !== false){
+					return ircclose(3,"IRC Server refused the connection.");
+				}elseif($data[0] == 'PING'){
+					if(!ircputs("PONG {$data[1]}")){return $ircret;}
+				}elseif(isval($data,1,'001')){
+					break;
+				}
+			}
+		}
+		if(!ircputs("IDENTIFY ".get_conf('rehash-pass','string')."\r\n")){return $ircret;}
+		while(!feof($irc)){
+			$line = fgets($irc,128);
+			if(is_string($line)){
+				$msg .= $line;
+				$data = explode(' ',$line);
+				if(isval($data,1,'433')){
+					return ircclose(4,"RehashServ is already running.");
+				}elseif(strrpos($line,'ERROR :Closing Link:') !== false){
+					return ircclose(3,"IRC Server refused the connection.");
+				}elseif(strrpos($line,":You are now identified for") !== false){
+					break;
+				}elseif(strrpos($line,'Password incorrect.') !== false){
+					return ircclose(5,"Failed to authenticate with NickServ");
+				}
+			}
+		}
+		if(!ircputs("HS ON\r\n")){return $ircret;}
+		while(!feof($irc)){
+			$line = fgets($irc,128);
+			if(is_string($line)){
+				$msg .= $line;
+				$data = explode(' ',$line);
+				if(isval($data,1,'433')){
+					return ircclose(4,"RehashServ is already running.");
+				}elseif(strrpos($line,'ERROR :Closing Link:') !== false){
+					return ircclose(3,"IRC Server refused the connection.");
+				}elseif(strrpos($line,':Your vhost of') !== false && strrpos($line,'is now activated') !== false){
+					break;
+				}elseif(strrpos($line,"Please contact an Operator to get a vhost assigned to this nick") !== false){
+					return ircclose(6,"vhost not set.");
+				}
+			}
+		}
+		if(!ircputs("OPER RehashServ ".get_conf('rehash-pass','string')."\r\n")){return $ircret;}
+		if(!ircputs("REHASH -global\r\n")){return $ircret;}
+		if(!ircputs("WALLOPS :{$u['nick']} has rehashed the server\r\n")){return $ircret;}
+		try{
+			error_reporting(0);
+			$msg .= 'QUIT :'.$message;
+			fputs($irc,'QUIT :'.$message);
+			error_reporting(E_ALL);
+		}catch(Exception $e){}
+		while(!feof($irc) && $line = fgets($irc,128)){
+			if(is_string($line)){
+				$msg .= $line;
+			}
+		}
+		fclose($irc);
+		if(strrpos($msg,':*** Notice -- Configuration loaded without any problems ..') === false){
+			return '{"code":6,"message":"There is an error in the config. See console for output.","log":'.json_encode($msg).'}';
+		}
+		return '{"code":0,"message":"Rehashed. View console for output.","log":'.json_encode($msg).'}';
+	}
+?>

+ 108 - 0
lib/opers.php

@@ -0,0 +1,108 @@
+<?php
+	require_once(dirname(dirname(__FILE__))."/header.php");
+	function get_opers_for_current_user_obj(){
+		global $user;
+		return get_opers_for_user_obj($user['id']);
+	}
+	function get_opers_obj(){
+		$opers = Array();
+		$res = query("SELECT o.id FROM opers o");
+		if($res && $res->num_rows != 0){
+			while($oper = $res->fetch_assoc()){
+				array_push($opers,get_oper_from_id_obj($oper['id']));
+			}
+		}
+		return $opers;
+	}
+	function get_opers_for_user_obj($id){
+		$opers = Array();
+		$res = query("SELECT o.id FROM opers o WHERE o.manager_id = %d OR o.user_id = %d",Array($id,$id));
+		if($res && $res->num_rows != 0){
+			while($oper = $res->fetch_assoc()){
+				array_push($opers,get_oper_from_id_obj($oper['id']));
+			}
+		}
+		return $opers;
+	}
+	function get_oper_from_id_obj($id){
+		$opers = Array();
+		$res = query("SELECT o.id,o.nick,o.password,o.password_type,o.swhois,o.flags,o.user_id,o.manager_id FROM opers_v o WHERE o.id = %d",Array($id));
+		if($res && $res->num_rows == 1){
+			$oper = $res->fetch_assoc();
+			$hosts = query("SELECT h.host FROM hosts h WHERE oper_id = %d",Array($oper['id']));
+			if($hosts->num_rows != 0){
+				$oper['hosts'] = Array();
+				while($host = $hosts->fetch_assoc()){
+					array_push($oper['hosts'],$host['host']);
+				}
+			}else{
+				$oper['hosts'] = Array('*@*');
+			}
+			if(!isset($oper['user_id'])){
+				$oper['user_id'] = '-';
+			}
+			return $oper;
+		}
+		return $opers;
+	}
+	function get_opers_html($opers){
+		global $u;
+		global $user;
+		if(!isset($u)){
+			$u = $user;
+		}
+		$ret = "";
+		foreach($opers as $k => $oper){
+			$ret .= "<h3>".($u['id'] != $oper['user_id']?"Managed Oper:":"Personal Oper:")."</h3>".get_form_html('oper-form-'.$oper['id'],Array(
+				Array(
+					'name'=>'nick',
+					'label'=>'Nick',
+					'type'=>'text',
+					'value'=>$oper['nick']
+				),
+				Array(
+					'name'=>'swhois',
+					'label'=>'Omnimaga Profile',
+					'type'=>'text',
+					'value'=>$oper['swhois']
+				),
+				Array(
+					'name'=>'password',
+					'label'=>'New Password',
+					'type'=>'password',
+					'value'=>''
+				),
+				Array(
+					'name'=>'id',
+					'type'=>'hidden',
+					'value'=>$oper['id']
+				),
+				Array(
+					'name'=>'action',
+					'type'=>'hidden',
+					'value'=>'oper'
+				)
+			),'Save')."<hr/>";
+		}
+		return $ret;
+	}
+	function get_opers_for_server_obj($id){
+		$opers = Array();
+		$res = query("SELECT o.id,o.nick,o.password,o.password_type,o.swhois,o.flags,o.role FROM opers_v o WHERE o.server_id = %d OR o.server_id IS NULL",Array($id));
+		if($res && $res->num_rows != 0){
+		while($oper = $res->fetch_assoc()){
+			$hosts = query("SELECT h.host FROM hosts h WHERE oper_id = %d",Array($oper['id']));
+			if($hosts->num_rows != 0){
+				$oper['hosts'] = Array();
+				while($host = $hosts->fetch_assoc()){
+					array_push($oper['hosts'],$host['host']);
+				}
+			}else{
+				$oper['hosts'] = Array('*@*');
+			}
+				array_push($opers,$oper);
+			}
+		}
+		return $opers;
+	}
+?>

+ 276 - 0
lib/security.php

@@ -0,0 +1,276 @@
+<?php
+	require_once(dirname(dirname(__FILE__))."/header.php");
+	require_once("configuration.php");
+	require_once("users.php");
+	switch(get_conf('2-factor-method')){
+		case 'authy':
+			require_once("authy-php/Authy.php");
+		//break;
+		case 'google-authenticator':
+			require_once("GoogleAuthenticator.php");
+		break;
+	}
+	function get_api(){
+		static $api;
+		if(!$api){
+			switch(get_conf('2-factor-method')){
+				case 'authy':
+					$api = new Authy_Api(get_conf('authy-api-key'),get_conf('authy-endpoint'));
+				break;
+				case 'google-authenticator':
+					$api = new PHPGangsta_GoogleAuthenticator();
+				break;
+			}
+		}
+		return $api;
+	}
+	function login($nick,$pass,$type,$effective_role=null){
+		if($type == 'user'){
+			$user = atheme_command(get_conf('xmlrpc-server'),get_conf('xmlrpc-port'),get_conf('xmlrpc-path'),USER_IP,$nick,$pass,'NickServ','info',Array($nick));
+			if($user[0]){
+				$user[2] = explode('&#10;',$user[1]);
+				$user[3] = Array();
+				foreach($user[2] as $k => $row){
+					$row = preg_split('/\s+:\s/',$row);
+					if(isset($row[1])){
+						$row[2] = explode(' ',$row[1]);
+					}else{
+						$row[1] = '';
+						$row[2] = Array();
+					}
+					$user[3][$row[0]] = Array($row[1],$row[2]);
+				}
+				$_SESSION['password'] = $pass;
+				$_SESSION['key'] = uniqid();
+				$_SESSION['real_name'] = $nick;
+				if(isset($user[3]['Email'][0])){
+					$_SESSION['email'] = $user[3]['Email'][0];
+				}else{
+					$_SESSION['email'] = '';
+				}
+				if($res = query("SELECT u.api_key, u.real_name FROM users u WHERE lower(u.nick) = lower('%s')",Array($nick))){
+					if($res->num_rows == 1){
+						$res = $res->fetch_assoc();
+						$_SESSION['key'] = $res['api_key'];
+						$_SESSION['real_name'] = $res['real_name'];
+					}
+				}
+				setcookie('key',$_SESSION['key'],null,'/');
+				setcookie('user',$nick,null,'/');
+				setcookie('type','user',null,'/');
+				return true;
+			}else{
+				return "Invalid credentials";
+			}
+		}elseif($type=='persona'){
+			if(!$user = get_user_obj($nick,$effective_role)){
+				return "User {$nick} does not exist";
+			}
+			if(!isset($_COOKIE['personaUser'])){
+				return false;
+			}
+			if(!in_array($_COOKIE['personaUser'],get_emails($user['id']))){
+				return "Invalid persona email";
+			}
+			setcookie('user',$nick,null,'/');
+			setcookie('key',$user['api_key'],null,'/');
+			setcookie('type',$user['type'],null,'/');
+			return true;
+		}else{
+			if(!$user = get_user_obj($nick,$type)){
+				return "User {$nick} does not exist";
+			}
+			if($user['password'] != mkpasswd($pass,$user['salt'])){
+				return "Invalid password";
+			}
+			setcookie('user',$nick,null,'/');
+			setcookie('key',$user['api_key'],null,'/');
+			setcookie('type',$user['type'],null,'/');
+			return true;
+		}
+	}
+	function verify($token){
+		$api = get_api();
+		if($u = is_logged_in()){
+			switch(get_conf('2-factor-method')){
+				case 'authy':
+					$verification = $api->verifyToken($u['secret_key'],$token);
+					if($verification->ok()){
+						setcookie('token',$u['secret_key'],null,'/');
+						$r = true;
+					}else{
+						$r = 'Failed to create Authy user: ';
+						foreach($verification->errors() as $field => $message){
+							$message = json_decode($message);
+							$r .= $message['message'];
+						}
+						logout();
+					}
+				break;
+				case 'google-authenticator':
+					if($api->verifyCode($u['secret_key'],$token,2)){
+						$_SESSION['secret_key'] = $u['secret_key'];
+						$r = true;
+					}else{
+						$r = "Token didn't match ".$u['secret_key'];
+					}
+				break;
+				default:
+					$r = true;
+			}
+		}else{
+			$r = "You have been logged out";
+		}
+		return $r;
+	}
+	function delete_token(){
+		$r = true;
+		$api = get_api();
+		$u = is_logged_in();
+		if($u){
+			switch(get_conf('2-factor-method')){
+				case 'authy':
+					$deletion = $api->deleteUser($u['secret_key']);
+					if($deletion->ok()){
+						setcookie('secret_key','',time() - 3600,'/');
+						if(!query("UPDATE users u SET u.secret_key=NULL WHERE u.id=%d",Array($u['id']))){
+							$r = 'Failed to disable 2-factor authentication';
+						}
+					}else{
+						$r = 'Failed to disable 2-factor authentication: ';
+						foreach($deletion->errors() as $field => $message){
+							$message = json_decode($message);
+							$r .= $message->message;
+						}
+					}
+				break;
+				case 'google-authenticator':
+					setcookie('secret_key','',time() - 3600,'/');
+					if(!query("UPDATE users u SET u.secret_key=NULL WHERE u.id=%d",Array($u['id']))){
+						$r = 'Failed to disable 2-factor authentication';
+					}
+				break;
+				default:
+			}
+		}
+		return $r;
+	}
+	function register_token(){
+		$api = get_api();
+		$u = is_logged_in();
+		if($u){
+			switch(get_conf('2-factor-method')){
+				case 'authy':
+					if(isset($_GET['country-code'])){
+						if(isset($_GET['cellphone'])){
+							$user = $api->registerUser($u['email'],$_GET['cellphone'],$_GET['country-code']);
+							if($user->ok()){
+								query("UPDATE users u SET u.secret_key='%s' WHERE u.id=%d",Array($user->id(),$u['id']));
+								$r = true;
+							}else{
+								$r = 'Failed to create Authy user: ';
+								foreach($user->errors() as $field => $message){
+									$message = json_decode($message);
+									$r .= $message['message'];
+								}
+							}
+						}else{
+							$r = "No cell number set";
+						}
+					}else{
+						$r = "No country code set";
+					}
+				break;
+				case 'google-authenticator':
+					if(isset($_GET['token'])){
+						if(isset($_SESSION['secret_key'])){
+							if($api->verifyCode($_SESSION['secret_key'],$_GET['token'], 2)){
+								query("UPDATE users u SET u.secret_key='%s' WHERE u.id=%d",Array($_SESSION['secret_key'],$u['id']));
+								$r = true;
+							}else{
+								$r = 'Could not register';
+							}
+						}else{
+							$r = 'No secret key defined';
+						}
+					}else{
+						$r = 'No token provided';
+					}
+				break;
+				default:
+					$r = true;
+			}
+		}else{
+			$r = "You have been logged out";
+		}
+		return $r;
+	}
+	function is_logged_in(){
+		$user = false;
+		if(isset($_COOKIE['user']) && isset($_COOKIE['key']) && isset($_COOKIE['type'])){
+			$user = get_user_obj($_COOKIE['user'],$_COOKIE['type']);
+			if(!$user || $user['api_key'] != $_COOKIE['key']){
+				$user = false;
+			}
+		}
+		return $user;
+	}
+	function is_verified(){
+		$api = get_api();
+		$user = is_logged_in();
+		$r = false;
+		if($user){
+			if(!isset($user['secret_key']) || is_null($user['secret_key']) || $user['secret_key'] == ''){
+				$r = true;
+			}else{
+				switch(get_conf('2-factor-method')){
+					case 'authy':
+						if(isset($_COOIKE['token']) && $user['secret_key'] == $_COOKIE['token']){
+							$r = true;
+						}
+					break;
+					case 'google-authenticator':
+						if(isset($_SESSION['secret_key']) && $_SESSION['secret_key'] == $user['secret_key']){
+							$r = true;
+						}
+					break;
+					default:
+						$r = true;
+				}
+			}
+		}
+		return $r;
+	}
+	function logout(){
+		setcookie('key','',time() - 3600,'/');
+		setcookie('user','',time() - 3600,'/');
+		setcookie('type','',time() - 3600,'/');
+		setcookie('personaUser','',time() - 3600,'/');
+		switch(get_conf('2-factor-method')){
+			case 'authy':break;
+			case 'google-authenticator':
+			default:
+				setcookie('token','',time() - 3600,'/');
+		}
+		unset($_SESSION['secret_key']);
+		if (isset($_COOKIE[session_name()])){
+			setcookie(session_name(),"",time()-3600,'/');
+		}
+		$_SESSION = array();
+		session_unset();
+		session_destroy();
+		session_write_close();
+		session_regenerate_id(true);
+	}
+	function mkpasswd($input,$salt=null){
+		$firsthash = pack("H*", sha1($input));
+		srand(time());
+		if($salt === null){
+			$salt = pack("c6", rand(0,255), rand(0,255), rand(0,255), rand(0,255), rand(0,255), rand(0,255));
+		}else{
+			$salt = base64_decode($salt);
+		}
+		$finalhash = pack("H*", sha1($firsthash.$salt));
+		return "$".base64_encode($salt)."$".base64_encode($finalhash);
+	}
+?>

+ 130 - 0
lib/servers.php

@@ -0,0 +1,130 @@
+<?php
+	require_once(dirname(dirname(__FILE__))."/header.php");
+	function build_server_tree(&$server,$depth=-1,$curdepth=0){
+		$server['children'] = Array();
+		 if($depth == -1 || $depth != $curdepth){
+			$children = query("SELECT s.id,s.description,s.host,s.name,s.ip,s.user_id,c.parent_id as parent FROM children_v c JOIN servers s ON c.child_id = s.id WHERE c.parent_id = %d",Array($server['id']));
+			if($children && $children->num_rows != 0 ){
+				while($row = $children->fetch_assoc()){
+					array_push($server['children'],$row);
+				}
+			}
+			foreach($server['children'] as $k => $child){
+				build_server_tree($server['children'][$k],$depth,$curdepth+1);
+			}
+		}
+		$parent = null;
+		if(!is_null($server['parent']) && $curdepth == 0){
+			$parent = query("SELECT s.id,s.description,s.host,s.name,s.user_id,s.ip FROM servers s WHERE s.id = '%s'",Array($server['parent']));
+			if($parent && $parent->num_rows == 1){
+				$parent = $parent->fetch_assoc();
+				$parent['opers'] = get_opers_for_server_obj($parent['id']);
+				$parent['children'] = Array();
+			}else{
+				$parent = null;
+			}
+		}
+		$server['parent'] = $parent;
+		$server['opers'] = get_opers_for_server_obj($server['id']);
+		return $server;
+	}
+	function get_current_server_obj(){
+		return get_server_obj($_GET['server']);
+	}
+	function get_servers_obj(){
+		$servers = Array();
+		$res = query("SELECT s.id FROM servers s");
+		if($res && $res->num_rows != 0){
+			while($server = $res->fetch_assoc()){
+				array_push($servers,get_server_for_id_obj($server['id']));
+			}
+		}
+		return $servers;
+	}
+	function get_ulines(){
+		$ulines = Array();
+		$res = query("SELECT u.host FROM ulines_v u");
+		if($res && $res->num_rows != 0){
+			while($server = $res->fetch_assoc()){
+				array_push($ulines,$server['host']);
+			}
+		}
+		return $ulines;
+	}
+	function get_ulines_obj(){
+		$ulines = Array();
+		$res = query("SELECT u.id FROM ulines_v u");
+		if($res && $res->num_rows != 0){
+			while($server = $res->fetch_assoc()){
+				array_push($ulines,get_server_for_id_obj($server['id']));
+			}
+		}
+		return $ulines;
+	}
+	function get_server_obj($name){
+		global $user;
+		$server = query("SELECT id,description,host,name,ip,user_id,parent_id as parent FROM servers s WHERE user_id = %d AND lower(name) = lower('%s')",Array($user['id'],$name));
+		if($server && $server->num_rows == 1){
+			$server = $server->fetch_assoc();
+			build_server_tree($server);
+			return $server;
+		}
+		return false;
+	}
+	function get_server_for_id_obj($id){
+		$server = query("SELECT id,description,host,name,ip,user_id,parent_id as parent FROM servers s WHERE s.id = %d",Array($id));
+		if($server && $server->num_rows == 1){
+			$server = $server->fetch_assoc();
+			build_server_tree($server);
+			return $server;
+		}
+		return false;
+	}
+	function get_servers_for_user_obj($id){
+		$res = query("SELECT id,description,host,name,ip,user_id,parent_id as parent FROM servers s WHERE user_id = %d",Array($id));
+		$servers = Array();
+		if($res && $res->num_rows != 0){
+			while($server = $res->fetch_assoc()){
+				build_server_tree($server);
+				array_push($servers,$server);
+			}
+		}
+		return $servers;
+	}
+	function get_servers_for_current_user_obj(){
+		global $user;
+		return get_servers_for_user_obj($user['id']);
+	}
+	function get_servers_list_html($servers,$depth=0){
+		global $user;
+		$r = '<div class="accordion">';
+		foreach($servers as $k => $server){
+			$r .= "<h3>{$server['name']} ({$server['host']}) - {$server['description']}</h3><div>";
+			$suser = get_user_from_id_obj($server['user_id']);
+			$r .= "<h4>Owner:</h4>{$suser['nick']}<ul>";
+			$r .= "<li>Real Name: {$suser['real_name']}</li><li>Email: {$suser['email']}</li></ul>";
+			$id = 0;
+			if(count($server['opers']) > 0){
+				$r .= "<h4>Opers:</h4><table class='tree'>";
+				$id++;
+				$pid = $id;
+				$r .= "<tr style='font-weight:bold;' class='treegrid-".($id)."'><td>Nick</td><td>Role</td></tr>";
+				foreach($server['opers'] as $kk => $oper){
+					$id++;
+					$r .= "<tr class='treegrid-{$id} treegrid-parent-{$pid}'><td>{$oper['nick']}</td><td>{$oper['role']}</td></tr>";
+				}
+				$r .= "</table>";
+			}
+			if(isset($server['parent'])){
+				$r .= "<h4>Parent</h4>";
+				$r .= get_servers_list_html(Array($server['parent']),$depth+1);
+			}
+			if(count($server['children']) > 0){
+				$r .= "<h4>Children</h4>";
+				$r .= get_servers_list_html($server['children'],$depth+1);
+			}
+			$r .="</div>";
+		}
+		return $r.'</div>';
+	}
+?>

+ 168 - 0
lib/users.php

@@ -0,0 +1,168 @@
+<?php
+	require_once(dirname(dirname(__FILE__))."/header.php");
+	function add_email($id,$email){
+		if(!in_array($email,get_emails($id))){
+			$res=query("INSERT INTO emails (user_id,email) VALUES (%d,'%s')",array($id,$email));
+			if(!$res){
+				return false;
+			}
+		}
+		return true;
+	}
+	function remove_email($user_id,$email,$is_id=false){
+		if($is_id){
+			if($res = query("DELETE FROM emails WHERE user_id = %d AND id = %d",array($user_id,$email))){
+				return true;
+			}
+		}else{
+			if($res = query("DELETE FROM emails WHERE user_id = %d AND email = '%s'",array($user_id,$email))){
+				return true;
+			}
+		}
+		return false;
+	}
+	function get_emails($id,$include_ids=false){
+		$emails = array();
+		if($res = query("SELECT e.email,e.id FROM emails e WHERE e.user_id = %d",array($id))){
+			while($email = $res->fetch_assoc()){
+				if($include_ids){
+					array_push($emails,$email);
+				}else{
+					array_push($emails,$email['email']);
+				}
+			}
+		}
+		return $emails;
+	}
+	function get_user_types($id){
+		$types = array();
+		if($res = query("SELECT t.name FROM user_roles r JOIN user_role_types t ON t.id = r.user_role_id WHERE r.user_id = %d GROUP BY r.user_role_id",array($id))){
+			while($type = $res->fetch_assoc()){
+				array_push($types,$type['name']);
+			}
+		}
+		array_push($types,'user');
+		return $types;
+	}
+	function get_user_for_email($email){
+		if($res = query("SELECT u.id FROM users u JOIN emails e ON e.user_id = u.id WHERE lower(e.email) = '%s'",Array($email))){
+			if($res->num_rows == 1){
+				$res = $res->fetch_assoc();
+				return get_user_from_id_obj($res['id']);
+			}
+		}
+		return false;
+	}
+	function get_current_user_obj($type){
+		$user = get_user_obj($_GET['user'],$type);
+		if($user && $user['api_key'] == $_GET['key']){
+			return $user;
+		}
+		return false;
+	}
+	function get_user_obj($nick,$type){
+		if($type == 'user' && isset($_SESSION['key']) && isset($_SESSION['password'])){
+			$user = Array(
+				'api_key'=>$_SESSION['key'],
+				'nick'=>$nick,
+				'password'=>$_SESSION['password'],
+				'flags'=>'u',
+				'id'=>'0',
+				'email'=>$_SESSION['email'],
+				'real_name'=>$_SESSION['real_name']
+			);
+			if($res = query("SELECT u.api_key,u.id,u.nick,u.real_name,u.email,u.password FROM users u WHERE lower(u.nick) = lower('%s')",Array($nick))){
+				if($res->num_rows == 1){
+					$res = $res->fetch_assoc();
+					foreach($res as $k => $attr){
+						if($k !== 'flags'){
+							$user[$k] = $attr;
+						}
+					}
+					$user['salt'] = substr($user['password'],1,strpos($user['password'],'$',1)-1);
+				}
+			}
+			return $user;
+		}else{
+			$user = query("SELECT u.api_key,u.id,u.nick,u.real_name,u.email,u.password,t.name AS type,t.flags AS flags,u.secret_key FROM ircd.users u JOIN ircd.user_roles r ON u.id = r.user_id JOIN ircd.user_role_types t ON r.user_role_id = t.id WHERE u.nick = '%s' AND t.name = '%s';",Array($nick,$type));
+			if($user && $user->num_rows == 1){
+				$user = $user->fetch_assoc();
+				$user['salt'] = substr($user['password'],1,strpos($user['password'],'$',1)-1);
+				return $user;
+			}
+		}
+		return false;
+	}
+	function get_user_nick($id){
+		$user = get_user_from_id_obj($id);
+		return $user['nick'];
+	}
+	function get_user_from_id_obj($id){
+		if($id === 0 && isset($_SESSION['key']) && isset($_SESSION['password'])){
+			$user = Array(
+				'api_key'=>$_SESSION['key'],
+				'nick'=>$_COOKIE['username'],
+				'password'=>$_SESSION['password'],
+				'flags'=>'u',
+				'id'=>'0',
+				'email'=>$_SESSION['email'],
+				'real_name'=>$_SESSION['real_name']
+			);
+			if($res = query("SELECT u.api_key,u.id,u.nick,u.real_name,u.email,u.password FROM users u WHERE lower(u.nick) = lower('%s')",Array($nick))){
+				if($res->num_rows == 1){
+					$res = $res->fetch_assoc();
+					foreach($res as $k => $attr){
+						if($k !== 'flags'){
+							$user[$k] = $attr;
+						}
+					}
+					$user['salt'] = substr($user['password'],1,strpos($user['password'],'$',1)-1);
+				}
+			}
+			return $user;
+		}else{
+			$user = query("SELECT u.api_key,u.id,u.nick,u.real_name,u.email,u.password,u.secret_key FROM ircd.users u where id = %d;",Array($id));
+			if($user && $user->num_rows == 1){
+				$user = $user->fetch_assoc();
+				$user['salt'] = substr($user['password'],1,strpos($user['password'],'$',1)-1);
+				return $user;
+			}
+		}
+		return false;
+	}
+	function get_user_html($user){
+		return get_form_html('user-form-'.$user['id'],Array(
+			Array(
+				'name'=>'real_name',
+				'label'=>'Real Name',
+				'type'=>'text',
+				'value'=>$user['real_name']
+			),
+			Array(
+				'name'=>'nick',
+				'label'=>'Nick',
+				'type'=>'text',
+				'value'=>$user['nick']
+			),
+			Array(
+				'name'=>'email',
+				'label'=>'Email',
+				'type'=>'text',
+				'value'=>$user['email']
+			),
+			Array(
+				'name'=>'id',
+				'type'=>'hidden',
+				'value'=>$user['id']
+			),
+			Array(
+				'name'=>'action',
+				'type'=>'hidden',
+				'value'=>'user'
+			)
+		),'Save');
+	}
+	function has_flag($user,$flag){
+		return strpos($user['flags'],$flag)!==false;
+	}
+?>

+ 3766 - 0
lib/xmlrpc.php

@@ -0,0 +1,3766 @@
+<?php
+// by Edd Dumbill (C) 1999-2002
+// <[email protected]>
+// $Id: xmlrpc.inc,v 1.174 2009/03/16 19:36:38 ggiunta Exp $
+
+// Copyright (c) 1999,2000,2002 Edd Dumbill.
+// 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 the "XML-RPC for PHP" 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
+// REGENTS 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.
+
+	if(!function_exists('xml_parser_create'))
+	{
+		// For PHP 4 onward, XML functionality is always compiled-in on windows:
+		// no more need to dl-open it. It might have been compiled out on *nix...
+		if(strtoupper(substr(PHP_OS, 0, 3) != 'WIN'))
+		{
+			dl('xml.so');
+		}
+	}
+
+	// G. Giunta 2005/01/29: declare global these variables,
+	// so that xmlrpc.inc will work even if included from within a function
+	// Milosch: 2005/08/07 - explicitly request these via $GLOBALS where used.
+	$GLOBALS['xmlrpcI4']='i4';
+	$GLOBALS['xmlrpcInt']='int';
+	$GLOBALS['xmlrpcBoolean']='boolean';
+	$GLOBALS['xmlrpcDouble']='double';
+	$GLOBALS['xmlrpcString']='string';
+	$GLOBALS['xmlrpcDateTime']='dateTime.iso8601';
+	$GLOBALS['xmlrpcBase64']='base64';
+	$GLOBALS['xmlrpcArray']='array';
+	$GLOBALS['xmlrpcStruct']='struct';
+	$GLOBALS['xmlrpcValue']='undefined';
+
+	$GLOBALS['xmlrpcTypes']=array(
+		$GLOBALS['xmlrpcI4']       => 1,
+		$GLOBALS['xmlrpcInt']      => 1,
+		$GLOBALS['xmlrpcBoolean']  => 1,
+		$GLOBALS['xmlrpcString']   => 1,
+		$GLOBALS['xmlrpcDouble']   => 1,
+		$GLOBALS['xmlrpcDateTime'] => 1,
+		$GLOBALS['xmlrpcBase64']   => 1,
+		$GLOBALS['xmlrpcArray']    => 2,
+		$GLOBALS['xmlrpcStruct']   => 3
+	);
+
+	$GLOBALS['xmlrpc_valid_parents'] = array(
+		'VALUE' => array('MEMBER', 'DATA', 'PARAM', 'FAULT'),
+		'BOOLEAN' => array('VALUE'),
+		'I4' => array('VALUE'),
+		'INT' => array('VALUE'),
+		'STRING' => array('VALUE'),
+		'DOUBLE' => array('VALUE'),
+		'DATETIME.ISO8601' => array('VALUE'),
+		'BASE64' => array('VALUE'),
+		'MEMBER' => array('STRUCT'),
+		'NAME' => array('MEMBER'),
+		'DATA' => array('ARRAY'),
+		'ARRAY' => array('VALUE'),
+		'STRUCT' => array('VALUE'),
+		'PARAM' => array('PARAMS'),
+		'METHODNAME' => array('METHODCALL'),
+		'PARAMS' => array('METHODCALL', 'METHODRESPONSE'),
+		'FAULT' => array('METHODRESPONSE'),
+		'NIL' => array('VALUE'), // only used when extension activated
+		'EX:NIL' => array('VALUE') // only used when extension activated
+	);
+
+	// define extra types for supporting NULL (useful for json or <NIL/>)
+	$GLOBALS['xmlrpcNull']='null';
+	$GLOBALS['xmlrpcTypes']['null']=1;
+
+	// tables used for transcoding different charsets into us-ascii xml
+
+	$GLOBALS['xml_iso88591_Entities']=array();
+	$GLOBALS['xml_iso88591_Entities']['in'] = array();
+	$GLOBALS['xml_iso88591_Entities']['out'] = array();
+	for ($i = 0; $i < 32; $i++)
+	{
+		$GLOBALS['xml_iso88591_Entities']['in'][] = chr($i);
+		$GLOBALS['xml_iso88591_Entities']['out'][] = '&#'.$i.';';
+	}
+	for ($i = 160; $i < 256; $i++)
+	{
+		$GLOBALS['xml_iso88591_Entities']['in'][] = chr($i);
+		$GLOBALS['xml_iso88591_Entities']['out'][] = '&#'.$i.';';
+	}
+
+	/// @todo add to iso table the characters from cp_1252 range, i.e. 128 to 159?
+	/// These will NOT be present in true ISO-8859-1, but will save the unwary
+	/// windows user from sending junk (though no luck when reciving them...)
+  /*
+	$GLOBALS['xml_cp1252_Entities']=array();
+	for ($i = 128; $i < 160; $i++)
+	{
+		$GLOBALS['xml_cp1252_Entities']['in'][] = chr($i);
+	}
+	$GLOBALS['xml_cp1252_Entities']['out'] = array(
+		'&#x20AC;', '?',        '&#x201A;', '&#x0192;',
+		'&#x201E;', '&#x2026;', '&#x2020;', '&#x2021;',
+		'&#x02C6;', '&#x2030;', '&#x0160;', '&#x2039;',
+		'&#x0152;', '?',        '&#x017D;', '?',
+		'?',        '&#x2018;', '&#x2019;', '&#x201C;',
+		'&#x201D;', '&#x2022;', '&#x2013;', '&#x2014;',
+		'&#x02DC;', '&#x2122;', '&#x0161;', '&#x203A;',
+		'&#x0153;', '?',        '&#x017E;', '&#x0178;'
+	);
+  */
+
+	$GLOBALS['xmlrpcerr'] = array(
+	'unknown_method'=>1,
+	'invalid_return'=>2,
+	'incorrect_params'=>3,
+	'introspect_unknown'=>4,
+	'http_error'=>5,
+	'no_data'=>6,
+	'no_ssl'=>7,
+	'curl_fail'=>8,
+	'invalid_request'=>15,
+	'no_curl'=>16,
+	'server_error'=>17,
+	'multicall_error'=>18,
+	'multicall_notstruct'=>9,
+	'multicall_nomethod'=>10,
+	'multicall_notstring'=>11,
+	'multicall_recursion'=>12,
+	'multicall_noparams'=>13,
+	'multicall_notarray'=>14,
+
+	'cannot_decompress'=>103,
+	'decompress_fail'=>104,
+	'dechunk_fail'=>105,
+	'server_cannot_decompress'=>106,
+	'server_decompress_fail'=>107
+	);
+
+	$GLOBALS['xmlrpcstr'] = array(
+	'unknown_method'=>'Unknown method',
+	'invalid_return'=>'Invalid return payload: enable debugging to examine incoming payload',
+	'incorrect_params'=>'Incorrect parameters passed to method',
+	'introspect_unknown'=>"Can't introspect: method unknown",
+	'http_error'=>"Didn't receive 200 OK from remote server.",
+	'no_data'=>'No data received from server.',
+	'no_ssl'=>'No SSL support compiled in.',
+	'curl_fail'=>'CURL error',
+	'invalid_request'=>'Invalid request payload',
+	'no_curl'=>'No CURL support compiled in.',
+	'server_error'=>'Internal server error',
+	'multicall_error'=>'Received from server invalid multicall response',
+	'multicall_notstruct'=>'system.multicall expected struct',
+	'multicall_nomethod'=>'missing methodName',
+	'multicall_notstring'=>'methodName is not a string',
+	'multicall_recursion'=>'recursive system.multicall forbidden',
+	'multicall_noparams'=>'missing params',
+	'multicall_notarray'=>'params is not an array',
+
+	'cannot_decompress'=>'Received from server compressed HTTP and cannot decompress',
+	'decompress_fail'=>'Received from server invalid compressed HTTP',
+	'dechunk_fail'=>'Received from server invalid chunked HTTP',
+	'server_cannot_decompress'=>'Received from client compressed HTTP request and cannot decompress',
+	'server_decompress_fail'=>'Received from client invalid compressed HTTP request'
+	);
+
+	// The charset encoding used by the server for received messages and
+	// by the client for received responses when received charset cannot be determined
+	// or is not supported
+	$GLOBALS['xmlrpc_defencoding']='UTF-8';
+
+	// The encoding used internally by PHP.
+	// String values received as xml will be converted to this, and php strings will be converted to xml
+	// as if having been coded with this
+	$GLOBALS['xmlrpc_internalencoding']='ISO-8859-1';
+
+	$GLOBALS['xmlrpcName']='XML-RPC for PHP';
+	$GLOBALS['xmlrpcVersion']='3.0.0.beta';
+
+	// let user errors start at 800
+	$GLOBALS['xmlrpcerruser']=800;
+	// let XML parse errors start at 100
+	$GLOBALS['xmlrpcerrxml']=100;
+
+	// formulate backslashes for escaping regexp
+	// Not in use anymore since 2.0. Shall we remove it?
+	/// @deprecated
+	$GLOBALS['xmlrpc_backslash']=chr(92).chr(92);
+
+	// set to TRUE to enable correct decoding of <NIL/> and <EX:NIL/> values
+	$GLOBALS['xmlrpc_null_extension']=false;
+
+	// set to TRUE to enable encoding of php NULL values to <EX:NIL/> instead of <NIL/>
+	$GLOBALS['xmlrpc_null_apache_encoding']=false;
+
+	// used to store state during parsing
+	// quick explanation of components:
+	//   ac - used to accumulate values
+	//   isf - used to indicate a parsing fault (2) or xmlrpcresp fault (1)
+	//   isf_reason - used for storing xmlrpcresp fault string
+	//   lv - used to indicate "looking for a value": implements
+	//        the logic to allow values with no types to be strings
+	//   params - used to store parameters in method calls
+	//   method - used to store method name
+	//   stack - array with genealogy of xml elements names:
+	//           used to validate nesting of xmlrpc elements
+	$GLOBALS['_xh']=null;
+
+	/**
+	* Convert a string to the correct XML representation in a target charset
+	* To help correct communication of non-ascii chars inside strings, regardless
+	* of the charset used when sending requests, parsing them, sending responses
+	* and parsing responses, an option is to convert all non-ascii chars present in the message
+	* into their equivalent 'charset entity'. Charset entities enumerated this way
+	* are independent of the charset encoding used to transmit them, and all XML
+	* parsers are bound to understand them.
+	* Note that in the std case we are not sending a charset encoding mime type
+	* along with http headers, so we are bound by RFC 3023 to emit strict us-ascii.
+	*
+	* @todo do a bit of basic benchmarking (strtr vs. str_replace)
+	* @todo	make usage of iconv() or recode_string() or mb_string() where available
+	*/
+	function xmlrpc_encode_entitites($data, $src_encoding='', $dest_encoding='')
+	{
+		if ($src_encoding == '')
+		{
+			// lame, but we know no better...
+			$src_encoding = $GLOBALS['xmlrpc_internalencoding'];
+		}
+
+		switch(strtoupper($src_encoding.'_'.$dest_encoding))
+		{
+			case 'ISO-8859-1_':
+			case 'ISO-8859-1_US-ASCII':
+				$escaped_data = str_replace(array('&', '"', "'", '<', '>'), array('&amp;', '&quot;', '&apos;', '&lt;', '&gt;'), $data);
+				$escaped_data = str_replace($GLOBALS['xml_iso88591_Entities']['in'], $GLOBALS['xml_iso88591_Entities']['out'], $escaped_data);
+				break;
+			case 'ISO-8859-1_UTF-8':
+				$escaped_data = str_replace(array('&', '"', "'", '<', '>'), array('&amp;', '&quot;', '&apos;', '&lt;', '&gt;'), $data);
+				$escaped_data = utf8_encode($escaped_data);
+				break;
+			case 'ISO-8859-1_ISO-8859-1':
+			case 'US-ASCII_US-ASCII':
+			case 'US-ASCII_UTF-8':
+			case 'US-ASCII_':
+			case 'US-ASCII_ISO-8859-1':
+			case 'UTF-8_UTF-8':
+			//case 'CP1252_CP1252':
+				$escaped_data = str_replace(array('&', '"', "'", '<', '>'), array('&amp;', '&quot;', '&apos;', '&lt;', '&gt;'), $data);
+				break;
+			case 'UTF-8_':
+			case 'UTF-8_US-ASCII':
+			case 'UTF-8_ISO-8859-1':
+	// NB: this will choke on invalid UTF-8, going most likely beyond EOF
+	$escaped_data = '';
+	// be kind to users creating string xmlrpcvals out of different php types
+	$data = (string) $data;
+	$ns = strlen ($data);
+	for ($nn = 0; $nn < $ns; $nn++)
+	{
+		$ch = $data[$nn];
+		$ii = ord($ch);
+		//1 7 0bbbbbbb (127)
+		if ($ii < 128)
+		{
+			/// @todo shall we replace this with a (supposedly) faster str_replace?
+			switch($ii){
+				case 34:
+					$escaped_data .= '&quot;';
+					break;
+				case 38:
+					$escaped_data .= '&amp;';
+					break;
+				case 39:
+					$escaped_data .= '&apos;';
+					break;
+				case 60:
+					$escaped_data .= '&lt;';
+					break;
+				case 62:
+					$escaped_data .= '&gt;';
+					break;
+				default:
+					$escaped_data .= $ch;
+			} // switch
+		}
+		//2 11 110bbbbb 10bbbbbb (2047)
+		else if ($ii>>5 == 6)
+		{
+			$b1 = ($ii & 31);
+			$ii = ord($data[$nn+1]);
+			$b2 = ($ii & 63);
+			$ii = ($b1 * 64) + $b2;
+			$ent = sprintf ('&#%d;', $ii);
+			$escaped_data .= $ent;
+			$nn += 1;
+		}
+		//3 16 1110bbbb 10bbbbbb 10bbbbbb
+		else if ($ii>>4 == 14)
+		{
+			$b1 = ($ii & 15);
+			$ii = ord($data[$nn+1]);
+			$b2 = ($ii & 63);
+			$ii = ord($data[$nn+2]);
+			$b3 = ($ii & 63);
+			$ii = ((($b1 * 64) + $b2) * 64) + $b3;
+			$ent = sprintf ('&#%d;', $ii);
+			$escaped_data .= $ent;
+			$nn += 2;
+		}
+		//4 21 11110bbb 10bbbbbb 10bbbbbb 10bbbbbb
+		else if ($ii>>3 == 30)
+		{
+			$b1 = ($ii & 7);
+			$ii = ord($data[$nn+1]);
+			$b2 = ($ii & 63);
+			$ii = ord($data[$nn+2]);
+			$b3 = ($ii & 63);
+			$ii = ord($data[$nn+3]);
+			$b4 = ($ii & 63);
+			$ii = ((((($b1 * 64) + $b2) * 64) + $b3) * 64) + $b4;
+			$ent = sprintf ('&#%d;', $ii);
+			$escaped_data .= $ent;
+			$nn += 3;
+		}
+	}
+				break;
+/*
+			case 'CP1252_':
+			case 'CP1252_US-ASCII':
+				$escaped_data = str_replace(array('&', '"', "'", '<', '>'), array('&amp;', '&quot;', '&apos;', '&lt;', '&gt;'), $data);
+				$escaped_data = str_replace($GLOBALS['xml_iso88591_Entities']['in'], $GLOBALS['xml_iso88591_Entities']['out'], $escaped_data);
+				$escaped_data = str_replace($GLOBALS['xml_cp1252_Entities']['in'], $GLOBALS['xml_cp1252_Entities']['out'], $escaped_data);
+				break;
+			case 'CP1252_UTF-8':
+				$escaped_data = str_replace(array('&', '"', "'", '<', '>'), array('&amp;', '&quot;', '&apos;', '&lt;', '&gt;'), $data);
+				/// @todo we could use real UTF8 chars here instead of xml entities... (note that utf_8 encode all allone will NOT convert them)
+				$escaped_data = str_replace($GLOBALS['xml_cp1252_Entities']['in'], $GLOBALS['xml_cp1252_Entities']['out'], $escaped_data);
+				$escaped_data = utf8_encode($escaped_data);
+				break;
+			case 'CP1252_ISO-8859-1':
+				$escaped_data = str_replace(array('&', '"', "'", '<', '>'), array('&amp;', '&quot;', '&apos;', '&lt;', '&gt;'), $data);
+				// we might as well replave all funky chars with a '?' here, but we are kind and leave it to the receiving application layer to decide what to do with these weird entities...
+				$escaped_data = str_replace($GLOBALS['xml_cp1252_Entities']['in'], $GLOBALS['xml_cp1252_Entities']['out'], $escaped_data);
+				break;
+*/
+			default:
+				$escaped_data = '';
+				error_log("Converting from $src_encoding to $dest_encoding: not supported...");
+		}
+		return $escaped_data;
+	}
+
+	/// xml parser handler function for opening element tags
+	function xmlrpc_se($parser, $name, $attrs, $accept_single_vals=false)
+	{
+		// if invalid xmlrpc already detected, skip all processing
+		if ($GLOBALS['_xh']['isf'] < 2)
+		{
+			// check for correct element nesting
+			// top level element can only be of 2 types
+			/// @todo optimization creep: save this check into a bool variable, instead of using count() every time:
+			///       there is only a single top level element in xml anyway
+			if (count($GLOBALS['_xh']['stack']) == 0)
+			{
+				if ($name != 'METHODRESPONSE' && $name != 'METHODCALL' && (
+					$name != 'VALUE' && !$accept_single_vals))
+				{
+					$GLOBALS['_xh']['isf'] = 2;
+					$GLOBALS['_xh']['isf_reason'] = 'missing top level xmlrpc element';
+					return;
+				}
+				else
+				{
+					$GLOBALS['_xh']['rt'] = strtolower($name);
+					$GLOBALS['_xh']['rt'] = strtolower($name);
+				}
+			}
+			else
+			{
+				// not top level element: see if parent is OK
+				$parent = end($GLOBALS['_xh']['stack']);
+				if (!array_key_exists($name, $GLOBALS['xmlrpc_valid_parents']) || !in_array($parent, $GLOBALS['xmlrpc_valid_parents'][$name]))
+				{
+					$GLOBALS['_xh']['isf'] = 2;
+					$GLOBALS['_xh']['isf_reason'] = "xmlrpc element $name cannot be child of $parent";
+					return;
+				}
+			}
+
+			switch($name)
+			{
+				// optimize for speed switch cases: most common cases first
+				case 'VALUE':
+					/// @todo we could check for 2 VALUE elements inside a MEMBER or PARAM element
+					$GLOBALS['_xh']['vt']='value'; // indicator: no value found yet
+					$GLOBALS['_xh']['ac']='';
+					$GLOBALS['_xh']['lv']=1;
+					$GLOBALS['_xh']['php_class']=null;
+					break;
+				case 'I4':
+				case 'INT':
+				case 'STRING':
+				case 'BOOLEAN':
+				case 'DOUBLE':
+				case 'DATETIME.ISO8601':
+				case 'BASE64':
+					if ($GLOBALS['_xh']['vt']!='value')
+					{
+						//two data elements inside a value: an error occurred!
+						$GLOBALS['_xh']['isf'] = 2;
+						$GLOBALS['_xh']['isf_reason'] = "$name element following a {$GLOBALS['_xh']['vt']} element inside a single value";
+						return;
+					}
+					$GLOBALS['_xh']['ac']=''; // reset the accumulator
+					break;
+				case 'STRUCT':
+				case 'ARRAY':
+					if ($GLOBALS['_xh']['vt']!='value')
+					{
+						//two data elements inside a value: an error occurred!
+						$GLOBALS['_xh']['isf'] = 2;
+						$GLOBALS['_xh']['isf_reason'] = "$name element following a {$GLOBALS['_xh']['vt']} element inside a single value";
+						return;
+					}
+					// create an empty array to hold child values, and push it onto appropriate stack
+					$cur_val = array();
+					$cur_val['values'] = array();
+					$cur_val['type'] = $name;
+					// check for out-of-band information to rebuild php objs
+					// and in case it is found, save it
+					if (@isset($attrs['PHP_CLASS']))
+					{
+						$cur_val['php_class'] = $attrs['PHP_CLASS'];
+					}
+					$GLOBALS['_xh']['valuestack'][] = $cur_val;
+					$GLOBALS['_xh']['vt']='data'; // be prepared for a data element next
+					break;
+				case 'DATA':
+					if ($GLOBALS['_xh']['vt']!='data')
+					{
+						//two data elements inside a value: an error occurred!
+						$GLOBALS['_xh']['isf'] = 2;
+						$GLOBALS['_xh']['isf_reason'] = "found two data elements inside an array element";
+						return;
+					}
+				case 'METHODCALL':
+				case 'METHODRESPONSE':
+				case 'PARAMS':
+					// valid elements that add little to processing
+					break;
+				case 'METHODNAME':
+				case 'NAME':
+					/// @todo we could check for 2 NAME elements inside a MEMBER element
+					$GLOBALS['_xh']['ac']='';
+					break;
+				case 'FAULT':
+					$GLOBALS['_xh']['isf']=1;
+					break;
+				case 'MEMBER':
+					$GLOBALS['_xh']['valuestack'][count($GLOBALS['_xh']['valuestack'])-1]['name']=''; // set member name to null, in case we do not find in the xml later on
+					//$GLOBALS['_xh']['ac']='';
+					// Drop trough intentionally
+				case 'PARAM':
+					// clear value type, so we can check later if no value has been passed for this param/member
+					$GLOBALS['_xh']['vt']=null;
+					break;
+				case 'NIL':
+				case 'EX:NIL':
+					if ($GLOBALS['xmlrpc_null_extension'])
+					{
+						if ($GLOBALS['_xh']['vt']!='value')
+						{
+							//two data elements inside a value: an error occurred!
+							$GLOBALS['_xh']['isf'] = 2;
+							$GLOBALS['_xh']['isf_reason'] = "$name element following a {$GLOBALS['_xh']['vt']} element inside a single value";
+							return;
+						}
+						$GLOBALS['_xh']['ac']=''; // reset the accumulator
+						break;
+					}
+					// we do not support the <NIL/> extension, so
+					// drop through intentionally
+				default:
+					/// INVALID ELEMENT: RAISE ISF so that it is later recognized!!!
+					$GLOBALS['_xh']['isf'] = 2;
+					$GLOBALS['_xh']['isf_reason'] = "found not-xmlrpc xml element $name";
+					break;
+			}
+
+			// Save current element name to stack, to validate nesting
+			$GLOBALS['_xh']['stack'][] = $name;
+
+			/// @todo optimization creep: move this inside the big switch() above
+			if($name!='VALUE')
+			{
+				$GLOBALS['_xh']['lv']=0;
+			}
+		}
+	}
+
+	/// Used in decoding xml chunks that might represent single xmlrpc values
+	function xmlrpc_se_any($parser, $name, $attrs)
+	{
+		xmlrpc_se($parser, $name, $attrs, true);
+	}
+
+	/// xml parser handler function for close element tags
+	function xmlrpc_ee($parser, $name, $rebuild_xmlrpcvals = true)
+	{
+		if ($GLOBALS['_xh']['isf'] < 2)
+		{
+			// push this element name from stack
+			// NB: if XML validates, correct opening/closing is guaranteed and
+			// we do not have to check for $name == $curr_elem.
+			// we also checked for proper nesting at start of elements...
+			$curr_elem = array_pop($GLOBALS['_xh']['stack']);
+
+			switch($name)
+			{
+				case 'VALUE':
+					// This if() detects if no scalar was inside <VALUE></VALUE>
+					if ($GLOBALS['_xh']['vt']=='value')
+					{
+						$GLOBALS['_xh']['value']=$GLOBALS['_xh']['ac'];
+						$GLOBALS['_xh']['vt']=$GLOBALS['xmlrpcString'];
+					}
+
+					if ($rebuild_xmlrpcvals)
+					{
+						// build the xmlrpc val out of the data received, and substitute it
+						$temp = new xmlrpcval($GLOBALS['_xh']['value'], $GLOBALS['_xh']['vt']);
+						// in case we got info about underlying php class, save it
+						// in the object we're rebuilding
+						if (isset($GLOBALS['_xh']['php_class']))
+							$temp->_php_class = $GLOBALS['_xh']['php_class'];
+						// check if we are inside an array or struct:
+						// if value just built is inside an array, let's move it into array on the stack
+						$vscount = count($GLOBALS['_xh']['valuestack']);
+						if ($vscount && $GLOBALS['_xh']['valuestack'][$vscount-1]['type']=='ARRAY')
+						{
+							$GLOBALS['_xh']['valuestack'][$vscount-1]['values'][] = $temp;
+						}
+						else
+						{
+							$GLOBALS['_xh']['value'] = $temp;
+						}
+					}
+					else
+					{
+						/// @todo this needs to treat correctly php-serialized objects,
+						/// since std deserializing is done by php_xmlrpc_decode,
+						/// which we will not be calling...
+						if (isset($GLOBALS['_xh']['php_class']))
+						{
+						}
+
+						// check if we are inside an array or struct:
+						// if value just built is inside an array, let's move it into array on the stack
+						$vscount = count($GLOBALS['_xh']['valuestack']);
+						if ($vscount && $GLOBALS['_xh']['valuestack'][$vscount-1]['type']=='ARRAY')
+						{
+							$GLOBALS['_xh']['valuestack'][$vscount-1]['values'][] = $GLOBALS['_xh']['value'];
+						}
+					}
+					break;
+				case 'BOOLEAN':
+				case 'I4':
+				case 'INT':
+				case 'STRING':
+				case 'DOUBLE':
+				case 'DATETIME.ISO8601':
+				case 'BASE64':
+					$GLOBALS['_xh']['vt']=strtolower($name);
+					/// @todo: optimization creep - remove the if/elseif cycle below
+					/// since the case() in which we are already did that
+					if ($name=='STRING')
+					{
+						$GLOBALS['_xh']['value']=$GLOBALS['_xh']['ac'];
+					}
+					elseif ($name=='DATETIME.ISO8601')
+					{
+						if (!preg_match('/^[0-9]{8}T[0-9]{2}:[0-9]{2}:[0-9]{2}$/', $GLOBALS['_xh']['ac']))
+						{
+							error_log('XML-RPC: invalid value received in DATETIME: '.$GLOBALS['_xh']['ac']);
+						}
+						$GLOBALS['_xh']['vt']=$GLOBALS['xmlrpcDateTime'];
+						$GLOBALS['_xh']['value']=$GLOBALS['_xh']['ac'];
+					}
+					elseif ($name=='BASE64')
+					{
+						/// @todo check for failure of base64 decoding / catch warnings
+						$GLOBALS['_xh']['value']=base64_decode($GLOBALS['_xh']['ac']);
+					}
+					elseif ($name=='BOOLEAN')
+					{
+						// special case here: we translate boolean 1 or 0 into PHP
+						// constants true or false.
+						// Strings 'true' and 'false' are accepted, even though the
+						// spec never mentions them (see eg. Blogger api docs)
+						// NB: this simple checks helps a lot sanitizing input, ie no
+						// security problems around here
+						if ($GLOBALS['_xh']['ac']=='1' || strcasecmp($GLOBALS['_xh']['ac'], 'true') == 0)
+						{
+							$GLOBALS['_xh']['value']=true;
+						}
+						else
+						{
+							// log if receiveing something strange, even though we set the value to false anyway
+							if ($GLOBALS['_xh']['ac']!='0' && strcasecmp($GLOBALS['_xh']['ac'], 'false') != 0)
+								error_log('XML-RPC: invalid value received in BOOLEAN: '.$GLOBALS['_xh']['ac']);
+							$GLOBALS['_xh']['value']=false;
+						}
+					}
+					elseif ($name=='DOUBLE')
+					{
+						// we have a DOUBLE
+						// we must check that only 0123456789-.<space> are characters here
+						// NOTE: regexp could be much stricter than this...
+						if (!preg_match('/^[+-eE0123456789 \t.]+$/', $GLOBALS['_xh']['ac']))
+						{
+							/// @todo: find a better way of throwing an error than this!
+							error_log('XML-RPC: non numeric value received in DOUBLE: '.$GLOBALS['_xh']['ac']);
+							$GLOBALS['_xh']['value']='ERROR_NON_NUMERIC_FOUND';
+						}
+						else
+						{
+							// it's ok, add it on
+							$GLOBALS['_xh']['value']=(double)$GLOBALS['_xh']['ac'];
+						}
+					}
+					else
+					{
+						// we have an I4/INT
+						// we must check that only 0123456789-<space> are characters here
+						if (!preg_match('/^[+-]?[0123456789 \t]+$/', $GLOBALS['_xh']['ac']))
+						{
+							/// @todo find a better way of throwing an error than this!
+							error_log('XML-RPC: non numeric value received in INT: '.$GLOBALS['_xh']['ac']);
+							$GLOBALS['_xh']['value']='ERROR_NON_NUMERIC_FOUND';
+						}
+						else
+						{
+							// it's ok, add it on
+							$GLOBALS['_xh']['value']=(int)$GLOBALS['_xh']['ac'];
+						}
+					}
+					//$GLOBALS['_xh']['ac']=''; // is this necessary?
+					$GLOBALS['_xh']['lv']=3; // indicate we've found a value
+					break;
+				case 'NAME':
+					$GLOBALS['_xh']['valuestack'][count($GLOBALS['_xh']['valuestack'])-1]['name'] = $GLOBALS['_xh']['ac'];
+					break;
+				case 'MEMBER':
+					//$GLOBALS['_xh']['ac']=''; // is this necessary?
+					// add to array in the stack the last element built,
+					// unless no VALUE was found
+					if ($GLOBALS['_xh']['vt'])
+					{
+						$vscount = count($GLOBALS['_xh']['valuestack']);
+						$GLOBALS['_xh']['valuestack'][$vscount-1]['values'][$GLOBALS['_xh']['valuestack'][$vscount-1]['name']] = $GLOBALS['_xh']['value'];
+					} else
+						error_log('XML-RPC: missing VALUE inside STRUCT in received xml');
+					break;
+				case 'DATA':
+					//$GLOBALS['_xh']['ac']=''; // is this necessary?
+					$GLOBALS['_xh']['vt']=null; // reset this to check for 2 data elements in a row - even if they're empty
+					break;
+				case 'STRUCT':
+				case 'ARRAY':
+					// fetch out of stack array of values, and promote it to current value
+					$curr_val = array_pop($GLOBALS['_xh']['valuestack']);
+					$GLOBALS['_xh']['value'] = $curr_val['values'];
+					$GLOBALS['_xh']['vt']=strtolower($name);
+					if (isset($curr_val['php_class']))
+					{
+						$GLOBALS['_xh']['php_class'] = $curr_val['php_class'];
+					}
+					break;
+				case 'PARAM':
+					// add to array of params the current value,
+					// unless no VALUE was found
+					if ($GLOBALS['_xh']['vt'])
+					{
+						$GLOBALS['_xh']['params'][]=$GLOBALS['_xh']['value'];
+						$GLOBALS['_xh']['pt'][]=$GLOBALS['_xh']['vt'];
+					}
+					else
+						error_log('XML-RPC: missing VALUE inside PARAM in received xml');
+					break;
+				case 'METHODNAME':
+					$GLOBALS['_xh']['method']=preg_replace('/^[\n\r\t ]+/', '', $GLOBALS['_xh']['ac']);
+					break;
+				case 'NIL':
+				case 'EX:NIL':
+					if ($GLOBALS['xmlrpc_null_extension'])
+					{
+						$GLOBALS['_xh']['vt']='null';
+						$GLOBALS['_xh']['value']=null;
+						$GLOBALS['_xh']['lv']=3;
+						break;
+					}
+					// drop through intentionally if nil extension not enabled
+				case 'PARAMS':
+				case 'FAULT':
+				case 'METHODCALL':
+				case 'METHORESPONSE':
+					break;
+				default:
+					// End of INVALID ELEMENT!
+					// shall we add an assert here for unreachable code???
+					break;
+			}
+		}
+	}
+
+	/// Used in decoding xmlrpc requests/responses without rebuilding xmlrpc values
+	function xmlrpc_ee_fast($parser, $name)
+	{
+		xmlrpc_ee($parser, $name, false);
+	}
+
+	/// xml parser handler function for character data
+	function xmlrpc_cd($parser, $data)
+	{
+		// skip processing if xml fault already detected
+		if ($GLOBALS['_xh']['isf'] < 2)
+		{
+			// "lookforvalue==3" means that we've found an entire value
+			// and should discard any further character data
+			if($GLOBALS['_xh']['lv']!=3)
+			{
+				// G. Giunta 2006-08-23: useless change of 'lv' from 1 to 2
+				//if($GLOBALS['_xh']['lv']==1)
+				//{
+					// if we've found text and we're just in a <value> then
+					// say we've found a value
+					//$GLOBALS['_xh']['lv']=2;
+				//}
+				// we always initialize the accumulator before starting parsing, anyway...
+				//if(!@isset($GLOBALS['_xh']['ac']))
+				//{
+				//	$GLOBALS['_xh']['ac'] = '';
+				//}
+				$GLOBALS['_xh']['ac'].=$data;
+			}
+		}
+	}
+
+	/// xml parser handler function for 'other stuff', ie. not char data or
+	/// element start/end tag. In fact it only gets called on unknown entities...
+	function xmlrpc_dh($parser, $data)
+	{
+		// skip processing if xml fault already detected
+		if ($GLOBALS['_xh']['isf'] < 2)
+		{
+			if(substr($data, 0, 1) == '&' && substr($data, -1, 1) == ';')
+			{
+				// G. Giunta 2006-08-25: useless change of 'lv' from 1 to 2
+				//if($GLOBALS['_xh']['lv']==1)
+				//{
+				//	$GLOBALS['_xh']['lv']=2;
+				//}
+				$GLOBALS['_xh']['ac'].=$data;
+			}
+		}
+		return true;
+	}
+
+	class xmlrpc_client
+	{
+		var $path;
+		var $server;
+		var $port=0;
+		var $method='http';
+		var $errno;
+		var $errstr;
+		var $debug=0;
+		var $username='';
+		var $password='';
+		var $authtype=1;
+		var $cert='';
+		var $certpass='';
+		var $cacert='';
+		var $cacertdir='';
+		var $key='';
+		var $keypass='';
+		var $verifypeer=true;
+		var $verifyhost=1;
+		var $no_multicall=false;
+		var $proxy='';
+		var $proxyport=0;
+		var $proxy_user='';
+		var $proxy_pass='';
+		var $proxy_authtype=1;
+		var $cookies=array();
+		var $extracurlopts=array();
+
+		/**
+		* List of http compression methods accepted by the client for responses.
+		* NB: PHP supports deflate, gzip compressions out of the box if compiled w. zlib
+		*
+		* NNB: you can set it to any non-empty array for HTTP11 and HTTPS, since
+		* in those cases it will be up to CURL to decide the compression methods
+		* it supports. You might check for the presence of 'zlib' in the output of
+		* curl_version() to determine wheter compression is supported or not
+		*/
+		var $accepted_compression = array();
+		/**
+		* Name of compression scheme to be used for sending requests.
+		* Either null, gzip or deflate
+		*/
+		var $request_compression = '';
+		/**
+		* CURL handle: used for keep-alive connections (PHP 4.3.8 up, see:
+		* http://curl.haxx.se/docs/faq.html#7.3)
+		*/
+		var $xmlrpc_curl_handle = null;
+		/// Wheter to use persistent connections for http 1.1 and https
+		var $keepalive = false;
+		/// Charset encodings that can be decoded without problems by the client
+		var $accepted_charset_encodings = array();
+		/// Charset encoding to be used in serializing request. NULL = use ASCII
+		var $request_charset_encoding = '';
+		/**
+		* Decides the content of xmlrpcresp objects returned by calls to send()
+		* valid strings are 'xmlrpcvals', 'phpvals' or 'xml'
+		*/
+		var $return_type = 'xmlrpcvals';
+		/**
+		* Sent to servers in http headers
+		*/
+		var $user_agent;
+
+		/**
+		* @param string $path either the complete server URL or the PATH part of the xmlrc server URL, e.g. /xmlrpc/server.php
+		* @param string $server the server name / ip address
+		* @param integer $port the port the server is listening on, defaults to 80 or 443 depending on protocol used
+		* @param string $method the http protocol variant: defaults to 'http', 'https' and 'http11' can be used if CURL is installed
+		*/
+		function xmlrpc_client($path, $server='', $port='', $method='')
+		{
+			// allow user to specify all params in $path
+			if($server == '' and $port == '' and $method == '')
+			{
+				$parts = parse_url($path);
+				$server = $parts['host'];
+				$path = isset($parts['path']) ? $parts['path'] : '';
+				if(isset($parts['query']))
+				{
+					$path .= '?'.$parts['query'];
+				}
+				if(isset($parts['fragment']))
+				{
+					$path .= '#'.$parts['fragment'];
+				}
+				if(isset($parts['port']))
+				{
+					$port = $parts['port'];
+				}
+				if(isset($parts['scheme']))
+				{
+					$method = $parts['scheme'];
+				}
+				if(isset($parts['user']))
+				{
+					$this->username = $parts['user'];
+				}
+				if(isset($parts['pass']))
+				{
+					$this->password = $parts['pass'];
+				}
+			}
+			if($path == '' || $path[0] != '/')
+			{
+				$this->path='/'.$path;
+			}
+			else
+			{
+				$this->path=$path;
+			}
+			$this->server=$server;
+			if($port != '')
+			{
+				$this->port=$port;
+			}
+			if($method != '')
+			{
+				$this->method=$method;
+			}
+
+			// if ZLIB is enabled, let the client by default accept compressed responses
+			if(function_exists('gzinflate') || (
+				function_exists('curl_init') && (($info = curl_version()) &&
+				((is_string($info) && strpos($info, 'zlib') !== null) || isset($info['libz_version'])))
+			))
+			{
+				$this->accepted_compression = array('gzip', 'deflate');
+			}
+
+			// keepalives: enabled by default
+			$this->keepalive = true;
+
+			// by default the xml parser can support these 3 charset encodings
+			$this->accepted_charset_encodings = array('UTF-8', 'ISO-8859-1', 'US-ASCII');
+
+			// initialize user_agent string
+			$this->user_agent = $GLOBALS['xmlrpcName'] . ' ' . $GLOBALS['xmlrpcVersion'];
+		}
+
+		/**
+		* Enables/disables the echoing to screen of the xmlrpc responses received
+		* @param integer $debug values 0, 1 and 2 are supported (2 = echo sent msg too, before received response)
+		* @access public
+		*/
+		function setDebug($in)
+		{
+			$this->debug=$in;
+		}
+
+		/**
+		* Add some http BASIC AUTH credentials, used by the client to authenticate
+		* @param string $u username
+		* @param string $p password
+		* @param integer $t auth type. See curl_setopt man page for supported auth types. Defaults to CURLAUTH_BASIC (basic auth)
+		* @access public
+		*/
+		function setCredentials($u, $p, $t=1)
+		{
+			$this->username=$u;
+			$this->password=$p;
+			$this->authtype=$t;
+		}
+
+		/**
+		* Add a client-side https certificate
+		* @param string $cert
+		* @param string $certpass
+		* @access public
+		*/
+		function setCertificate($cert, $certpass)
+		{
+			$this->cert = $cert;
+			$this->certpass = $certpass;
+		}
+
+		/**
+		* Add a CA certificate to verify server with (see man page about
+		* CURLOPT_CAINFO for more details
+		* @param string $cacert certificate file name (or dir holding certificates)
+		* @param bool $is_dir set to true to indicate cacert is a dir. defaults to false
+		* @access public
+		*/
+		function setCaCertificate($cacert, $is_dir=false)
+		{
+			if ($is_dir)
+			{
+				$this->cacertdir = $cacert;
+			}
+			else
+			{
+				$this->cacert = $cacert;
+			}
+		}
+
+		/**
+		* Set attributes for SSL communication: private SSL key
+		* NB: does not work in older php/curl installs
+		* Thanks to Daniel Convissor
+		* @param string $key The name of a file containing a private SSL key
+		* @param string $keypass The secret password needed to use the private SSL key
+		* @access public
+		*/
+		function setKey($key, $keypass)
+		{
+			$this->key = $key;
+			$this->keypass = $keypass;
+		}
+
+		/**
+		* Set attributes for SSL communication: verify server certificate
+		* @param bool $i enable/disable verification of peer certificate
+		* @access public
+		*/
+		function setSSLVerifyPeer($i)
+		{
+			$this->verifypeer = $i;
+		}
+
+		/**
+		* Set attributes for SSL communication: verify match of server cert w. hostname
+		* @param int $i
+		* @access public
+		*/
+		function setSSLVerifyHost($i)
+		{
+			$this->verifyhost = $i;
+		}
+
+		/**
+		* Set proxy info
+		* @param string $proxyhost
+		* @param string $proxyport Defaults to 8080 for HTTP and 443 for HTTPS
+		* @param string $proxyusername Leave blank if proxy has public access
+		* @param string $proxypassword Leave blank if proxy has public access
+		* @param int $proxyauthtype set to constant CURLAUTH_NTLM to use NTLM auth with proxy
+		* @access public
+		*/
+		function setProxy($proxyhost, $proxyport, $proxyusername = '', $proxypassword = '', $proxyauthtype = 1)
+		{
+			$this->proxy = $proxyhost;
+			$this->proxyport = $proxyport;
+			$this->proxy_user = $proxyusername;
+			$this->proxy_pass = $proxypassword;
+			$this->proxy_authtype = $proxyauthtype;
+		}
+
+		/**
+		* Enables/disables reception of compressed xmlrpc responses.
+		* Note that enabling reception of compressed responses merely adds some standard
+		* http headers to xmlrpc requests. It is up to the xmlrpc server to return
+		* compressed responses when receiving such requests.
+		* @param string $compmethod either 'gzip', 'deflate', 'any' or ''
+		* @access public
+		*/
+		function setAcceptedCompression($compmethod)
+		{
+			if ($compmethod == 'any')
+				$this->accepted_compression = array('gzip', 'deflate');
+			else
+				$this->accepted_compression = array($compmethod);
+		}
+
+		/**
+		* Enables/disables http compression of xmlrpc request.
+		* Take care when sending compressed requests: servers might not support them
+		* (and automatic fallback to uncompressed requests is not yet implemented)
+		* @param string $compmethod either 'gzip', 'deflate' or ''
+		* @access public
+		*/
+		function setRequestCompression($compmethod)
+		{
+			$this->request_compression = $compmethod;
+		}
+
+		/**
+		* Adds a cookie to list of cookies that will be sent to server.
+		* NB: setting any param but name and value will turn the cookie into a 'version 1' cookie:
+		* do not do it unless you know what you are doing
+		* @param string $name
+		* @param string $value
+		* @param string $path
+		* @param string $domain
+		* @param int $port
+		* @access public
+		*
+		* @todo check correctness of urlencoding cookie value (copied from php way of doing it...)
+		*/
+		function setCookie($name, $value='', $path='', $domain='', $port=null)
+		{
+			$this->cookies[$name]['value'] = urlencode($value);
+			if ($path || $domain || $port)
+			{
+				$this->cookies[$name]['path'] = $path;
+				$this->cookies[$name]['domain'] = $domain;
+				$this->cookies[$name]['port'] = $port;
+				$this->cookies[$name]['version'] = 1;
+			}
+			else
+			{
+				$this->cookies[$name]['version'] = 0;
+			}
+		}
+
+		/**
+		* Directly set cURL options, for extra flexibility
+		* It allows eg. to bind client to a specific IP interface / address
+		* @param $options array
+		*/
+		function SetCurlOptions( $options )
+		{
+			$this->extracurlopts = $options;
+		}
+
+		/**
+		* Set user-agent string that will be used by this client instance
+		* in http headers sent to the server
+		*/
+		function SetUserAgent( $agentstring )
+		{
+			$this->user_agent = $agentstring;
+		}
+
+		/**
+		* Send an xmlrpc request
+		* @param mixed $msg The message object, or an array of messages for using multicall, or the complete xml representation of a request
+		* @param integer $timeout Connection timeout, in seconds, If unspecified, a platform specific timeout will apply
+		* @param string $method if left unspecified, the http protocol chosen during creation of the object will be used
+		* @return xmlrpcresp
+		* @access public
+		*/
+		function& send($msg, $timeout=0, $method='')
+		{
+			// if user deos not specify http protocol, use native method of this client
+			// (i.e. method set during call to constructor)
+			if($method == '')
+			{
+				$method = $this->method;
+			}
+
+			if(is_array($msg))
+			{
+				// $msg is an array of xmlrpcmsg's
+				$r = $this->multicall($msg, $timeout, $method);
+				return $r;
+			}
+			elseif(is_string($msg))
+			{
+				$n = new xmlrpcmsg('');
+				$n->payload = $msg;
+				$msg = $n;
+			}
+
+			// where msg is an xmlrpcmsg
+			$msg->debug=$this->debug;
+
+			if($method == 'https')
+			{
+				$r =& $this->sendPayloadHTTPS(
+					$msg,
+					$this->server,
+					$this->port,
+					$timeout,
+					$this->username,
+					$this->password,
+					$this->authtype,
+					$this->cert,
+					$this->certpass,
+					$this->cacert,
+					$this->cacertdir,
+					$this->proxy,
+					$this->proxyport,
+					$this->proxy_user,
+					$this->proxy_pass,
+					$this->proxy_authtype,
+					$this->keepalive,
+					$this->key,
+					$this->keypass
+				);
+			}
+			elseif($method == 'http11')
+			{
+				$r =& $this->sendPayloadCURL(
+					$msg,
+					$this->server,
+					$this->port,
+					$timeout,
+					$this->username,
+					$this->password,
+					$this->authtype,
+					null,
+					null,
+					null,
+					null,
+					$this->proxy,
+					$this->proxyport,
+					$this->proxy_user,
+					$this->proxy_pass,
+					$this->proxy_authtype,
+					'http',
+					$this->keepalive
+				);
+			}
+			else
+			{
+				$r =& $this->sendPayloadHTTP10(
+					$msg,
+					$this->server,
+					$this->port,
+					$timeout,
+					$this->username,
+					$this->password,
+					$this->authtype,
+					$this->proxy,
+					$this->proxyport,
+					$this->proxy_user,
+					$this->proxy_pass,
+					$this->proxy_authtype
+				);
+			}
+
+			return $r;
+		}
+
+		/**
+		* @access private
+		*/
+		function &sendPayloadHTTP10($msg, $server, $port, $timeout=0,
+			$username='', $password='', $authtype=1, $proxyhost='',
+			$proxyport=0, $proxyusername='', $proxypassword='', $proxyauthtype=1)
+		{
+			if($port==0)
+			{
+				$port=80;
+			}
+
+			// Only create the payload if it was not created previously
+			if(empty($msg->payload))
+			{
+				$msg->createPayload($this->request_charset_encoding);
+			}
+
+			$payload = $msg->payload;
+			// Deflate request body and set appropriate request headers
+			if(function_exists('gzdeflate') && ($this->request_compression == 'gzip' || $this->request_compression == 'deflate'))
+			{
+				if($this->request_compression == 'gzip')
+				{
+					$a = @gzencode($payload);
+					if($a)
+					{
+						$payload = $a;
+						$encoding_hdr = "Content-Encoding: gzip\r\n";
+					}
+				}
+				else
+				{
+					$a = @gzcompress($payload);
+					if($a)
+					{
+						$payload = $a;
+						$encoding_hdr = "Content-Encoding: deflate\r\n";
+					}
+				}
+			}
+			else
+			{
+				$encoding_hdr = '';
+			}
+
+			// thanks to Grant Rauscher <[email protected]> for this
+			$credentials='';
+			if($username!='')
+			{
+				$credentials='Authorization: Basic ' . base64_encode($username . ':' . $password) . "\r\n";
+				if ($authtype != 1)
+				{
+					error_log('XML-RPC: '.__METHOD__.': warning. Only Basic auth is supported with HTTP 1.0');
+				}
+			}
+
+			$accepted_encoding = '';
+			if(is_array($this->accepted_compression) && count($this->accepted_compression))
+			{
+				$accepted_encoding = 'Accept-Encoding: ' . implode(', ', $this->accepted_compression) . "\r\n";
+			}
+
+			$proxy_credentials = '';
+			if($proxyhost)
+			{
+				if($proxyport == 0)
+				{
+					$proxyport = 8080;
+				}
+				$connectserver = $proxyhost;
+				$connectport = $proxyport;
+				$uri = 'http://'.$server.':'.$port.$this->path;
+				if($proxyusername != '')
+				{
+					if ($proxyauthtype != 1)
+					{
+						error_log('XML-RPC: '.__METHOD__.': warning. Only Basic auth to proxy is supported with HTTP 1.0');
+					}
+					$proxy_credentials = 'Proxy-Authorization: Basic ' . base64_encode($proxyusername.':'.$proxypassword) . "\r\n";
+				}
+			}
+			else
+			{
+				$connectserver = $server;
+				$connectport = $port;
+				$uri = $this->path;
+			}
+
+			// Cookie generation, as per rfc2965 (version 1 cookies) or
+			// netscape's rules (version 0 cookies)
+			$cookieheader='';
+			if (count($this->cookies))
+			{
+				$version = '';
+				foreach ($this->cookies as $name => $cookie)
+				{
+					if ($cookie['version'])
+					{
+						$version = ' $Version="' . $cookie['version'] . '";';
+						$cookieheader .= ' ' . $name . '="' . $cookie['value'] . '";';
+						if ($cookie['path'])
+							$cookieheader .= ' $Path="' . $cookie['path'] . '";';
+						if ($cookie['domain'])
+							$cookieheader .= ' $Domain="' . $cookie['domain'] . '";';
+						if ($cookie['port'])
+							$cookieheader .= ' $Port="' . $cookie['port'] . '";';
+					}
+					else
+					{
+						$cookieheader .= ' ' . $name . '=' . $cookie['value'] . ";";
+					}
+				}
+				$cookieheader = 'Cookie:' . $version . substr($cookieheader, 0, -1) . "\r\n";
+			}
+
+			$op= 'POST ' . $uri. " HTTP/1.0\r\n" .
+				'User-Agent: ' . $this->user_agent . "\r\n" .
+				'Host: '. $server . ':' . $port . "\r\n" .
+				$credentials .
+				$proxy_credentials .
+				$accepted_encoding .
+				$encoding_hdr .
+				'Accept-Charset: ' . implode(',', $this->accepted_charset_encodings) . "\r\n" .
+				$cookieheader .
+				'Content-Type: ' . $msg->content_type . "\r\nContent-Length: " .
+				strlen($payload) . "\r\n\r\n" .
+				$payload;
+
+			if($this->debug > 1)
+			{
+				print "<PRE>\n---SENDING---\n" . htmlentities($op) . "\n---END---\n</PRE>";
+				// let the client see this now in case http times out...
+				flush();
+			}
+
+			if($timeout>0)
+			{
+				$fp=@fsockopen($connectserver, $connectport, $this->errno, $this->errstr, $timeout);
+			}
+			else
+			{
+				$fp=@fsockopen($connectserver, $connectport, $this->errno, $this->errstr);
+			}
+			if($fp)
+			{
+				if($timeout>0 && function_exists('stream_set_timeout'))
+				{
+					stream_set_timeout($fp, $timeout);
+				}
+			}
+			else
+			{
+				$this->errstr='Connect error: '.$this->errstr;
+				$r=new xmlrpcresp(0, $GLOBALS['xmlrpcerr']['http_error'], $this->errstr . ' (' . $this->errno . ')');
+				return $r;
+			}
+
+			if(!fputs($fp, $op, strlen($op)))
+			{
+				fclose($fp);
+				$this->errstr='Write error';
+				$r=new xmlrpcresp(0, $GLOBALS['xmlrpcerr']['http_error'], $this->errstr);
+				return $r;
+			}
+			else
+			{
+				// reset errno and errstr on succesful socket connection
+				$this->errstr = '';
+			}
+			// G. Giunta 2005/10/24: close socket before parsing.
+			// should yeld slightly better execution times, and make easier recursive calls (e.g. to follow http redirects)
+			$ipd='';
+			do
+			{
+				// shall we check for $data === FALSE?
+				// as per the manual, it signals an error
+				$ipd.=fread($fp, 32768);
+			} while(!feof($fp));
+			fclose($fp);
+			$r =& $msg->parseResponse($ipd, false, $this->return_type);
+			return $r;
+
+		}
+
+		/**
+		* @access private
+		*/
+		function &sendPayloadHTTPS($msg, $server, $port, $timeout=0, $username='',
+			$password='', $authtype=1, $cert='',$certpass='', $cacert='', $cacertdir='',
+			$proxyhost='', $proxyport=0, $proxyusername='', $proxypassword='', $proxyauthtype=1,
+			$keepalive=false, $key='', $keypass='')
+		{
+			$r =& $this->sendPayloadCURL($msg, $server, $port, $timeout, $username,
+				$password, $authtype, $cert, $certpass, $cacert, $cacertdir, $proxyhost, $proxyport,
+				$proxyusername, $proxypassword, $proxyauthtype, 'https', $keepalive, $key, $keypass);
+			return $r;
+		}
+
+		/**
+		* Contributed by Justin Miller <[email protected]>
+		* Requires curl to be built into PHP
+		* NB: CURL versions before 7.11.10 cannot use proxy to talk to https servers!
+		* @access private
+		*/
+		function &sendPayloadCURL($msg, $server, $port, $timeout=0, $username='',
+			$password='', $authtype=1, $cert='', $certpass='', $cacert='', $cacertdir='',
+			$proxyhost='', $proxyport=0, $proxyusername='', $proxypassword='', $proxyauthtype=1, $method='https',
+			$keepalive=false, $key='', $keypass='')
+		{
+			if(!function_exists('curl_init'))
+			{
+				$this->errstr='CURL unavailable on this install';
+				$r=new xmlrpcresp(0, $GLOBALS['xmlrpcerr']['no_curl'], $GLOBALS['xmlrpcstr']['no_curl']);
+				return $r;
+			}
+			if($method == 'https')
+			{
+				if(($info = curl_version()) &&
+					((is_string($info) && strpos($info, 'OpenSSL') === null) || (is_array($info) && !isset($info['ssl_version']))))
+				{
+					$this->errstr='SSL unavailable on this install';
+					$r=new xmlrpcresp(0, $GLOBALS['xmlrpcerr']['no_ssl'], $GLOBALS['xmlrpcstr']['no_ssl']);
+					return $r;
+				}
+			}
+
+			if($port == 0)
+			{
+				if($method == 'http')
+				{
+					$port = 80;
+				}
+				else
+				{
+					$port = 443;
+				}
+			}
+
+			// Only create the payload if it was not created previously
+			if(empty($msg->payload))
+			{
+				$msg->createPayload($this->request_charset_encoding);
+			}
+
+			// Deflate request body and set appropriate request headers
+			$payload = $msg->payload;
+			if(function_exists('gzdeflate') && ($this->request_compression == 'gzip' || $this->request_compression == 'deflate'))
+			{
+				if($this->request_compression == 'gzip')
+				{
+					$a = @gzencode($payload);
+					if($a)
+					{
+						$payload = $a;
+						$encoding_hdr = 'Content-Encoding: gzip';
+					}
+				}
+				else
+				{
+					$a = @gzcompress($payload);
+					if($a)
+					{
+						$payload = $a;
+						$encoding_hdr = 'Content-Encoding: deflate';
+					}
+				}
+			}
+			else
+			{
+				$encoding_hdr = '';
+			}
+
+			if($this->debug > 1)
+			{
+				print "<PRE>\n---SENDING---\n" . htmlentities($payload) . "\n---END---\n</PRE>";
+				// let the client see this now in case http times out...
+				flush();
+			}
+
+			if(!$keepalive || !$this->xmlrpc_curl_handle)
+			{
+				$curl = curl_init($method . '://' . $server . ':' . $port . $this->path);
+				if($keepalive)
+				{
+					$this->xmlrpc_curl_handle = $curl;
+				}
+			}
+			else
+			{
+				$curl = $this->xmlrpc_curl_handle;
+			}
+
+			// results into variable
+			curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
+
+			if($this->debug)
+			{
+				curl_setopt($curl, CURLOPT_VERBOSE, 1);
+			}
+			curl_setopt($curl, CURLOPT_USERAGENT, $this->user_agent);
+			// required for XMLRPC: post the data
+			curl_setopt($curl, CURLOPT_POST, 1);
+			// the data
+			curl_setopt($curl, CURLOPT_POSTFIELDS, $payload);
+
+			// return the header too
+			curl_setopt($curl, CURLOPT_HEADER, 1);
+
+			// will only work with PHP >= 5.0
+			// NB: if we set an empty string, CURL will add http header indicating
+			// ALL methods it is supporting. This is possibly a better option than
+			// letting the user tell what curl can / cannot do...
+			if(is_array($this->accepted_compression) && count($this->accepted_compression))
+			{
+				//curl_setopt($curl, CURLOPT_ENCODING, implode(',', $this->accepted_compression));
+				// empty string means 'any supported by CURL' (shall we catch errors in case CURLOPT_SSLKEY undefined ?)
+				if (count($this->accepted_compression) == 1)
+				{
+					curl_setopt($curl, CURLOPT_ENCODING, $this->accepted_compression[0]);
+				}
+				else
+					curl_setopt($curl, CURLOPT_ENCODING, '');
+			}
+			// extra headers
+			$headers = array('Content-Type: ' . $msg->content_type , 'Accept-Charset: ' . implode(',', $this->accepted_charset_encodings));
+			// if no keepalive is wanted, let the server know it in advance
+			if(!$keepalive)
+			{
+				$headers[] = 'Connection: close';
+			}
+			// request compression header
+			if($encoding_hdr)
+			{
+				$headers[] = $encoding_hdr;
+			}
+
+			curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
+			// timeout is borked
+			if($timeout)
+			{
+				curl_setopt($curl, CURLOPT_TIMEOUT, $timeout == 1 ? 1 : $timeout - 1);
+			}
+
+			if($username && $password)
+			{
+				curl_setopt($curl, CURLOPT_USERPWD, $username.':'.$password);
+				if (defined('CURLOPT_HTTPAUTH'))
+				{
+					curl_setopt($curl, CURLOPT_HTTPAUTH, $authtype);
+				}
+				else if ($authtype != 1)
+				{
+					error_log('XML-RPC: '.__METHOD__.': warning. Only Basic auth is supported by the current PHP/curl install');
+				}
+			}
+
+			if($method == 'https')
+			{
+				// set cert file
+				if($cert)
+				{
+					curl_setopt($curl, CURLOPT_SSLCERT, $cert);
+				}
+				// set cert password
+				if($certpass)
+				{
+					curl_setopt($curl, CURLOPT_SSLCERTPASSWD, $certpass);
+				}
+				// whether to verify remote host's cert
+				curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $this->verifypeer);
+				// set ca certificates file/dir
+				if($cacert)
+				{
+					curl_setopt($curl, CURLOPT_CAINFO, $cacert);
+				}
+				if($cacertdir)
+				{
+					curl_setopt($curl, CURLOPT_CAPATH, $cacertdir);
+				}
+				// set key file (shall we catch errors in case CURLOPT_SSLKEY undefined ?)
+				if($key)
+				{
+					curl_setopt($curl, CURLOPT_SSLKEY, $key);
+				}
+				// set key password (shall we catch errors in case CURLOPT_SSLKEY undefined ?)
+				if($keypass)
+				{
+					curl_setopt($curl, CURLOPT_SSLKEYPASSWD, $keypass);
+				}
+				// whether to verify cert's common name (CN); 0 for no, 1 to verify that it exists, and 2 to verify that it matches the hostname used
+				curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, $this->verifyhost);
+			}
+
+			// proxy info
+			if($proxyhost)
+			{
+				if($proxyport == 0)
+				{
+					$proxyport = 8080; // NB: even for HTTPS, local connection is on port 8080
+				}
+				curl_setopt($curl, CURLOPT_PROXY, $proxyhost.':'.$proxyport);
+				//curl_setopt($curl, CURLOPT_PROXYPORT,$proxyport);
+				if($proxyusername)
+				{
+					curl_setopt($curl, CURLOPT_PROXYUSERPWD, $proxyusername.':'.$proxypassword);
+					if (defined('CURLOPT_PROXYAUTH'))
+					{
+						curl_setopt($curl, CURLOPT_PROXYAUTH, $proxyauthtype);
+					}
+					else if ($proxyauthtype != 1)
+					{
+						error_log('XML-RPC: '.__METHOD__.': warning. Only Basic auth to proxy is supported by the current PHP/curl install');
+					}
+				}
+			}
+
+			// NB: should we build cookie http headers by hand rather than let CURL do it?
+			// the following code does not honour 'expires', 'path' and 'domain' cookie attributes
+			// set to client obj the the user...
+			if (count($this->cookies))
+			{
+				$cookieheader = '';
+				foreach ($this->cookies as $name => $cookie)
+				{
+					$cookieheader .= $name . '=' . $cookie['value'] . '; ';
+				}
+				curl_setopt($curl, CURLOPT_COOKIE, substr($cookieheader, 0, -2));
+			}
+
+			foreach ($this->extracurlopts as $opt => $val)
+			{
+				curl_setopt($curl, $opt, $val);
+			}
+
+			$result = curl_exec($curl);
+
+			if ($this->debug > 1)
+			{
+				print "<PRE>\n---CURL INFO---\n";
+				foreach(curl_getinfo($curl) as $name => $val)
+					 print $name . ': ' . htmlentities($val). "\n";
+				print "---END---\n</PRE>";
+			}
+
+			if(!$result) /// @todo we should use a better check here - what if we get back '' or '0'?
+			{
+				$this->errstr='no response';
+				$resp=new xmlrpcresp(0, $GLOBALS['xmlrpcerr']['curl_fail'], $GLOBALS['xmlrpcstr']['curl_fail']. ': '. curl_error($curl));
+				curl_close($curl);
+				if($keepalive)
+				{
+					$this->xmlrpc_curl_handle = null;
+				}
+			}
+			else
+			{
+				if(!$keepalive)
+				{
+					curl_close($curl);
+				}
+				$resp =& $msg->parseResponse($result, true, $this->return_type);
+			}
+			return $resp;
+		}
+
+		/**
+		* Send an array of request messages and return an array of responses.
+		* Unless $this->no_multicall has been set to true, it will try first
+		* to use one single xmlrpc call to server method system.multicall, and
+		* revert to sending many successive calls in case of failure.
+		* This failure is also stored in $this->no_multicall for subsequent calls.
+		* Unfortunately, there is no server error code universally used to denote
+		* the fact that multicall is unsupported, so there is no way to reliably
+		* distinguish between that and a temporary failure.
+		* If you are sure that server supports multicall and do not want to
+		* fallback to using many single calls, set the fourth parameter to FALSE.
+		*
+		* NB: trying to shoehorn extra functionality into existing syntax has resulted
+		* in pretty much convoluted code...
+		*
+		* @param array $msgs an array of xmlrpcmsg objects
+		* @param integer $timeout connection timeout (in seconds)
+		* @param string $method the http protocol variant to be used
+		* @param boolean fallback When true, upon receiveing an error during multicall, multiple single calls will be attempted
+		* @return array
+		* @access public
+		*/
+		function multicall($msgs, $timeout=0, $method='', $fallback=true)
+		{
+			if ($method == '')
+			{
+				$method = $this->method;
+			}
+			if(!$this->no_multicall)
+			{
+				$results = $this->_try_multicall($msgs, $timeout, $method);
+				if(is_array($results))
+				{
+					// System.multicall succeeded
+					return $results;
+				}
+				else
+				{
+					// either system.multicall is unsupported by server,
+					// or call failed for some other reason.
+					if ($fallback)
+					{
+						// Don't try it next time...
+						$this->no_multicall = true;
+					}
+					else
+					{
+						if (is_a($results, 'xmlrpcresp'))
+						{
+							$result = $results;
+						}
+						else
+						{
+							$result = new xmlrpcresp(0, $GLOBALS['xmlrpcerr']['multicall_error'], $GLOBALS['xmlrpcstr']['multicall_error']);
+						}
+					}
+				}
+			}
+			else
+			{
+				// override fallback, in case careless user tries to do two
+				// opposite things at the same time
+				$fallback = true;
+			}
+
+			$results = array();
+			if ($fallback)
+			{
+				// system.multicall is (probably) unsupported by server:
+				// emulate multicall via multiple requests
+				foreach($msgs as $msg)
+				{
+					$results[] =& $this->send($msg, $timeout, $method);
+				}
+			}
+			else
+			{
+				// user does NOT want to fallback on many single calls:
+				// since we should always return an array of responses,
+				// return an array with the same error repeated n times
+				foreach($msgs as $msg)
+				{
+					$results[] = $result;
+				}
+			}
+			return $results;
+		}
+
+		/**
+		* Attempt to boxcar $msgs via system.multicall.
+		* Returns either an array of xmlrpcreponses, an xmlrpc error response
+		* or false (when received response does not respect valid multicall syntax)
+		* @access private
+		*/
+		function _try_multicall($msgs, $timeout, $method)
+		{
+			// Construct multicall message
+			$calls = array();
+			foreach($msgs as $msg)
+			{
+				$call['methodName'] = new xmlrpcval($msg->method(),'string');
+				$numParams = $msg->getNumParams();
+				$params = array();
+				for($i = 0; $i < $numParams; $i++)
+				{
+					$params[$i] = $msg->getParam($i);
+				}
+				$call['params'] = new xmlrpcval($params, 'array');
+				$calls[] = new xmlrpcval($call, 'struct');
+			}
+			$multicall = new xmlrpcmsg('system.multicall');
+			$multicall->addParam(new xmlrpcval($calls, 'array'));
+
+			// Attempt RPC call
+			$result =& $this->send($multicall, $timeout, $method);
+
+			if($result->faultCode() != 0)
+			{
+				// call to system.multicall failed
+				return $result;
+			}
+
+			// Unpack responses.
+			$rets = $result->value();
+
+			if ($this->return_type == 'xml')
+			{
+					return $rets;
+			}
+			else if ($this->return_type == 'phpvals')
+			{
+				///@todo test this code branch...
+				$rets = $result->value();
+				if(!is_array($rets))
+				{
+					return false;		// bad return type from system.multicall
+				}
+				$numRets = count($rets);
+				if($numRets != count($msgs))
+				{
+					return false;		// wrong number of return values.
+				}
+
+				$response = array();
+				for($i = 0; $i < $numRets; $i++)
+				{
+					$val = $rets[$i];
+					if (!is_array($val)) {
+						return false;
+					}
+					switch(count($val))
+					{
+						case 1:
+							if(!isset($val[0]))
+							{
+								return false;		// Bad value
+							}
+							// Normal return value
+							$response[$i] = new xmlrpcresp($val[0], 0, '', 'phpvals');
+							break;
+						case 2:
+							///	@todo remove usage of @: it is apparently quite slow
+							$code = @$val['faultCode'];
+							if(!is_int($code))
+							{
+								return false;
+							}
+							$str = @$val['faultString'];
+							if(!is_string($str))
+							{
+								return false;
+							}
+							$response[$i] = new xmlrpcresp(0, $code, $str);
+							break;
+						default:
+							return false;
+					}
+				}
+				return $response;
+			}
+			else // return type == 'xmlrpcvals'
+			{
+				$rets = $result->value();
+				if($rets->kindOf() != 'array')
+				{
+					return false;		// bad return type from system.multicall
+				}
+				$numRets = $rets->arraysize();
+				if($numRets != count($msgs))
+				{
+					return false;		// wrong number of return values.
+				}
+
+				$response = array();
+				for($i = 0; $i < $numRets; $i++)
+				{
+					$val = $rets->arraymem($i);
+					switch($val->kindOf())
+					{
+						case 'array':
+							if($val->arraysize() != 1)
+							{
+								return false;		// Bad value
+							}
+							// Normal return value
+							$response[$i] = new xmlrpcresp($val->arraymem(0));
+							break;
+						case 'struct':
+							$code = $val->structmem('faultCode');
+							if($code->kindOf() != 'scalar' || $code->scalartyp() != 'int')
+							{
+								return false;
+							}
+							$str = $val->structmem('faultString');
+							if($str->kindOf() != 'scalar' || $str->scalartyp() != 'string')
+							{
+								return false;
+							}
+							$response[$i] = new xmlrpcresp(0, $code->scalarval(), $str->scalarval());
+							break;
+						default:
+							return false;
+					}
+				}
+				return $response;
+			}
+		}
+	} // end class xmlrpc_client
+
+	class xmlrpcresp
+	{
+		var $val = 0;
+		var $valtyp;
+		var $errno = 0;
+		var $errstr = '';
+		var $payload;
+		var $hdrs = array();
+		var $_cookies = array();
+		var $content_type = 'text/xml';
+		var $raw_data = '';
+
+		/**
+		* @param mixed $val either an xmlrpcval obj, a php value or the xml serialization of an xmlrpcval (a string)
+		* @param integer $fcode set it to anything but 0 to create an error response
+		* @param string $fstr the error string, in case of an error response
+		* @param string $valtyp either 'xmlrpcvals', 'phpvals' or 'xml'
+		*
+		* @todo add check that $val / $fcode / $fstr is of correct type???
+		* NB: as of now we do not do it, since it might be either an xmlrpcval or a plain
+		* php val, or a complete xml chunk, depending on usage of xmlrpc_client::send() inside which creator is called...
+		*/
+		function xmlrpcresp($val, $fcode = 0, $fstr = '', $valtyp='')
+		{
+			if($fcode != 0)
+			{
+				// error response
+				$this->errno = $fcode;
+				$this->errstr = $fstr;
+				//$this->errstr = htmlspecialchars($fstr); // XXX: encoding probably shouldn't be done here; fix later.
+			}
+			else
+			{
+				// successful response
+				$this->val = $val;
+				if ($valtyp == '')
+				{
+					// user did not declare type of response value: try to guess it
+					if (is_object($this->val) && is_a($this->val, 'xmlrpcval'))
+					{
+						$this->valtyp = 'xmlrpcvals';
+					}
+					else if (is_string($this->val))
+					{
+						$this->valtyp = 'xml';
+
+					}
+					else
+					{
+						$this->valtyp = 'phpvals';
+					}
+				}
+				else
+				{
+					// user declares type of resp value: believe him
+					$this->valtyp = $valtyp;
+				}
+			}
+		}
+
+		/**
+		* Returns the error code of the response.
+		* @return integer the error code of this response (0 for not-error responses)
+		* @access public
+		*/
+		function faultCode()
+		{
+			return $this->errno;
+		}
+
+		/**
+		* Returns the error code of the response.
+		* @return string the error string of this response ('' for not-error responses)
+		* @access public
+		*/
+		function faultString()
+		{
+			return $this->errstr;
+		}
+
+		/**
+		* Returns the value received by the server.
+		* @return mixed the xmlrpcval object returned by the server. Might be an xml string or php value if the response has been created by specially configured xmlrpc_client objects
+		* @access public
+		*/
+		function value()
+		{
+			return $this->val;
+		}
+
+		/**
+		* Returns an array with the cookies received from the server.
+		* Array has the form: $cookiename => array ('value' => $val, $attr1 => $val1, $attr2 = $val2, ...)
+		* with attributes being e.g. 'expires', 'path', domain'.
+		* NB: cookies sent as 'expired' by the server (i.e. with an expiry date in the past)
+		* are still present in the array. It is up to the user-defined code to decide
+		* how to use the received cookies, and wheter they have to be sent back with the next
+		* request to the server (using xmlrpc_client::setCookie) or not
+		* @return array array of cookies received from the server
+		* @access public
+		*/
+		function cookies()
+		{
+			return $this->_cookies;
+		}
+
+		/**
+		* Returns xml representation of the response. XML prologue not included
+		* @param string $charset_encoding the charset to be used for serialization. if null, US-ASCII is assumed
+		* @return string the xml representation of the response
+		* @access public
+		*/
+		function serialize($charset_encoding='')
+		{
+			if ($charset_encoding != '')
+				$this->content_type = 'text/xml; charset=' . $charset_encoding;
+			else
+				$this->content_type = 'text/xml';
+			$result = "<methodResponse>\n";
+			if($this->errno)
+			{
+				// G. Giunta 2005/2/13: let non-ASCII response messages be tolerated by clients
+				// by xml-encoding non ascii chars
+				$result .= "<fault>\n" .
+"<value>\n<struct><member><name>faultCode</name>\n<value><int>" . $this->errno .
+"</int></value>\n</member>\n<member>\n<name>faultString</name>\n<value><string>" .
+xmlrpc_encode_entitites($this->errstr, $GLOBALS['xmlrpc_internalencoding'], $charset_encoding) . "</string></value>\n</member>\n" .
+"</struct>\n</value>\n</fault>";
+			}
+			else
+			{
+				if(!is_object($this->val) || !is_a($this->val, 'xmlrpcval'))
+				{
+					if (is_string($this->val) && $this->valtyp == 'xml')
+					{
+						$result .= "<params>\n<param>\n" .
+							$this->val .
+							"</param>\n</params>";
+					}
+					else
+					{
+						/// @todo try to build something serializable?
+						die('cannot serialize xmlrpcresp objects whose content is native php values');
+					}
+				}
+				else
+				{
+					$result .= "<params>\n<param>\n" .
+						$this->val->serialize($charset_encoding) .
+						"</param>\n</params>";
+				}
+			}
+			$result .= "\n</methodResponse>";
+			$this->payload = $result;
+			return $result;
+		}
+	}
+
+	class xmlrpcmsg
+	{
+		var $payload;
+		var $methodname;
+		var $params=array();
+		var $debug=0;
+		var $content_type = 'text/xml';
+
+		/**
+		* @param string $meth the name of the method to invoke
+		* @param array $pars array of parameters to be paased to the method (xmlrpcval objects)
+		*/
+		function xmlrpcmsg($meth, $pars=0)
+		{
+			$this->methodname=$meth;
+			if(is_array($pars) && count($pars)>0)
+			{
+				for($i=0; $i<count($pars); $i++)
+				{
+					$this->addParam($pars[$i]);
+				}
+			}
+		}
+
+		/**
+		* @access private
+		*/
+		function xml_header($charset_encoding='')
+		{
+			if ($charset_encoding != '')
+			{
+				return "<?xml version=\"1.0\" encoding=\"$charset_encoding\" ?" . ">\n<methodCall>\n";
+			}
+			else
+			{
+				return "<?xml version=\"1.0\"?" . ">\n<methodCall>\n";
+			}
+		}
+
+		/**
+		* @access private
+		*/
+		function xml_footer()
+		{
+			return '</methodCall>';
+		}
+
+		/**
+		* @access private
+		*/
+		function kindOf()
+		{
+			return 'msg';
+		}
+
+		/**
+		* @access private
+		*/
+		function createPayload($charset_encoding='')
+		{
+			if ($charset_encoding != '')
+				$this->content_type = 'text/xml; charset=' . $charset_encoding;
+			else
+				$this->content_type = 'text/xml';
+			$this->payload=$this->xml_header($charset_encoding);
+			$this->payload.='<methodName>' . $this->methodname . "</methodName>\n";
+			$this->payload.="<params>\n";
+			for($i=0; $i<count($this->params); $i++)
+			{
+				$p=$this->params[$i];
+				$this->payload.="<param>" . $p->serialize($charset_encoding) .
+				"</param>\n";
+			}
+			$this->payload.="</params>\n";
+			$this->payload.=$this->xml_footer();
+		}
+
+		/**
+		* Gets/sets the xmlrpc method to be invoked
+		* @param string $meth the method to be set (leave empty not to set it)
+		* @return string the method that will be invoked
+		* @access public
+		*/
+		function method($meth='')
+		{
+			if($meth!='')
+			{
+				$this->methodname=$meth;
+			}
+			return $this->methodname;
+		}
+
+		/**
+		* Returns xml representation of the message. XML prologue included
+		* @return string the xml representation of the message, xml prologue included
+		* @access public
+		*/
+		function serialize($charset_encoding='')
+		{
+			$this->createPayload($charset_encoding);
+			return $this->payload;
+		}
+
+		/**
+		* Add a parameter to the list of parameters to be used upon method invocation
+		* @param xmlrpcval $par
+		* @return boolean false on failure
+		* @access public
+		*/
+		function addParam($par)
+		{
+			// add check: do not add to self params which are not xmlrpcvals
+			if(is_object($par) && is_a($par, 'xmlrpcval'))
+			{
+				$this->params[]=$par;
+				return true;
+			}
+			else
+			{
+				return false;
+			}
+		}
+
+		/**
+		* Returns the nth parameter in the message. The index zero-based.
+		* @param integer $i the index of the parameter to fetch (zero based)
+		* @return xmlrpcval the i-th parameter
+		* @access public
+		*/
+		function getParam($i) { return $this->params[$i]; }
+
+		/**
+		* Returns the number of parameters in the messge.
+		* @return integer the number of parameters currently set
+		* @access public
+		*/
+		function getNumParams() { return count($this->params); }
+
+		/**
+		* Given an open file handle, read all data available and parse it as axmlrpc response.
+		* NB: the file handle is not closed by this function.
+		* NNB: might have trouble in rare cases to work on network streams, as we
+		*      check for a read of 0 bytes instead of feof($fp).
+		*      But since checking for feof(null) returns false, we would risk an
+		*      infinite loop in that case, because we cannot trust the caller
+		*      to give us a valid pointer to an open file...
+		* @access public
+		* @return xmlrpcresp
+		* @todo add 2nd & 3rd param to be passed to ParseResponse() ???
+		*/
+		function &parseResponseFile($fp)
+		{
+			$ipd='';
+			while($data=fread($fp, 32768))
+			{
+				$ipd.=$data;
+			}
+			//fclose($fp);
+			$r =& $this->parseResponse($ipd);
+			return $r;
+		}
+
+		/**
+		* Parses HTTP headers and separates them from data.
+		* @access private
+		*/
+		function &parseResponseHeaders(&$data, $headers_processed=false)
+		{
+				// Support "web-proxy-tunelling" connections for https through proxies
+				if(preg_match('/^HTTP\/1\.[0-1] 200 Connection established/', $data))
+				{
+					// Look for CR/LF or simple LF as line separator,
+					// (even though it is not valid http)
+					$pos = strpos($data,"\r\n\r\n");
+					if($pos || is_int($pos))
+					{
+						$bd = $pos+4;
+					}
+					else
+					{
+						$pos = strpos($data,"\n\n");
+						if($pos || is_int($pos))
+						{
+							$bd = $pos+2;
+						}
+						else
+						{
+							// No separation between response headers and body: fault?
+							$bd = 0;
+						}
+					}
+					if ($bd)
+					{
+						// this filters out all http headers from proxy.
+						// maybe we could take them into account, too?
+						$data = substr($data, $bd);
+					}
+					else
+					{
+						error_log('XML-RPC: '.__METHOD__.': HTTPS via proxy error, tunnel connection possibly failed');
+						$r=new xmlrpcresp(0, $GLOBALS['xmlrpcerr']['http_error'], $GLOBALS['xmlrpcstr']['http_error']. ' (HTTPS via proxy error, tunnel connection possibly failed)');
+						return $r;
+					}
+				}
+
+				// Strip HTTP 1.1 100 Continue header if present
+				while(preg_match('/^HTTP\/1\.1 1[0-9]{2} /', $data))
+				{
+					$pos = strpos($data, 'HTTP', 12);
+					// server sent a Continue header without any (valid) content following...
+					// give the client a chance to know it
+					if(!$pos && !is_int($pos)) // works fine in php 3, 4 and 5
+					{
+						break;
+					}
+					$data = substr($data, $pos);
+				}
+				if(!preg_match('/^HTTP\/[0-9.]+ 200 /', $data))
+				{
+					$errstr= substr($data, 0, strpos($data, "\n")-1);
+					error_log('XML-RPC: '.__METHOD__.': HTTP error, got response: ' .$errstr);
+					$r=new xmlrpcresp(0, $GLOBALS['xmlrpcerr']['http_error'], $GLOBALS['xmlrpcstr']['http_error']. ' (' . $errstr . ')');
+					return $r;
+				}
+
+				$GLOBALS['_xh']['headers'] = array();
+				$GLOBALS['_xh']['cookies'] = array();
+
+				// be tolerant to usage of \n instead of \r\n to separate headers and data
+				// (even though it is not valid http)
+				$pos = strpos($data,"\r\n\r\n");
+				if($pos || is_int($pos))
+				{
+					$bd = $pos+4;
+				}
+				else
+				{
+					$pos = strpos($data,"\n\n");
+					if($pos || is_int($pos))
+					{
+						$bd = $pos+2;
+					}
+					else
+					{
+						// No separation between response headers and body: fault?
+						// we could take some action here instead of going on...
+						$bd = 0;
+					}
+				}
+				// be tolerant to line endings, and extra empty lines
+				$ar = preg_split("/\r?\n/", trim(substr($data, 0, $pos)));
+				while(list(,$line) = @each($ar))
+				{
+					// take care of multi-line headers and cookies
+					$arr = explode(':',$line,2);
+					if(count($arr) > 1)
+					{
+						$header_name = strtolower(trim($arr[0]));
+						/// @todo some other headers (the ones that allow a CSV list of values)
+						/// do allow many values to be passed using multiple header lines.
+						/// We should add content to $GLOBALS['_xh']['headers'][$header_name]
+						/// instead of replacing it for those...
+						if ($header_name == 'set-cookie' || $header_name == 'set-cookie2')
+						{
+							if ($header_name == 'set-cookie2')
+							{
+								// version 2 cookies:
+								// there could be many cookies on one line, comma separated
+								$cookies = explode(',', $arr[1]);
+							}
+							else
+							{
+								$cookies = array($arr[1]);
+							}
+							foreach ($cookies as $cookie)
+							{
+								// glue together all received cookies, using a comma to separate them
+								// (same as php does with getallheaders())
+								if (isset($GLOBALS['_xh']['headers'][$header_name]))
+									$GLOBALS['_xh']['headers'][$header_name] .= ', ' . trim($cookie);
+								else
+									$GLOBALS['_xh']['headers'][$header_name] = trim($cookie);
+								// parse cookie attributes, in case user wants to correctly honour them
+								// feature creep: only allow rfc-compliant cookie attributes?
+								// @todo support for server sending multiple time cookie with same name, but using different PATHs
+								$cookie = explode(';', $cookie);
+								foreach ($cookie as $pos => $val)
+								{
+									$val = explode('=', $val, 2);
+									$tag = trim($val[0]);
+									$val = trim(@$val[1]);
+									/// @todo with version 1 cookies, we should strip leading and trailing " chars
+									if ($pos == 0)
+									{
+										$cookiename = $tag;
+										$GLOBALS['_xh']['cookies'][$tag] = array();
+										$GLOBALS['_xh']['cookies'][$cookiename]['value'] = urldecode($val);
+									}
+									else
+									{
+										if ($tag != 'value')
+										{
+										  $GLOBALS['_xh']['cookies'][$cookiename][$tag] = $val;
+										}
+									}
+								}
+							}
+						}
+						else
+						{
+							$GLOBALS['_xh']['headers'][$header_name] = trim($arr[1]);
+						}
+					}
+					elseif(isset($header_name))
+					{
+						///	@todo version1 cookies might span multiple lines, thus breaking the parsing above
+						$GLOBALS['_xh']['headers'][$header_name] .= ' ' . trim($line);
+					}
+				}
+
+				$data = substr($data, $bd);
+
+				if($this->debug && count($GLOBALS['_xh']['headers']))
+				{
+					print '<PRE>';
+					foreach($GLOBALS['_xh']['headers'] as $header => $value)
+					{
+						print htmlentities("HEADER: $header: $value\n");
+					}
+					foreach($GLOBALS['_xh']['cookies'] as $header => $value)
+					{
+						print htmlentities("COOKIE: $header={$value['value']}\n");
+					}
+					print "</PRE>\n";
+				}
+
+				// if CURL was used for the call, http headers have been processed,
+				// and dechunking + reinflating have been carried out
+				if(!$headers_processed)
+				{
+					// Decode chunked encoding sent by http 1.1 servers
+					if(isset($GLOBALS['_xh']['headers']['transfer-encoding']) && $GLOBALS['_xh']['headers']['transfer-encoding'] == 'chunked')
+					{
+						if(!$data = decode_chunked($data))
+						{
+							error_log('XML-RPC: '.__METHOD__.': errors occurred when trying to rebuild the chunked data received from server');
+							$r = new xmlrpcresp(0, $GLOBALS['xmlrpcerr']['dechunk_fail'], $GLOBALS['xmlrpcstr']['dechunk_fail']);
+							return $r;
+						}
+					}
+
+					// Decode gzip-compressed stuff
+					// code shamelessly inspired from nusoap library by Dietrich Ayala
+					if(isset($GLOBALS['_xh']['headers']['content-encoding']))
+					{
+						$GLOBALS['_xh']['headers']['content-encoding'] = str_replace('x-', '', $GLOBALS['_xh']['headers']['content-encoding']);
+						if($GLOBALS['_xh']['headers']['content-encoding'] == 'deflate' || $GLOBALS['_xh']['headers']['content-encoding'] == 'gzip')
+						{
+							// if decoding works, use it. else assume data wasn't gzencoded
+							if(function_exists('gzinflate'))
+							{
+								if($GLOBALS['_xh']['headers']['content-encoding'] == 'deflate' && $degzdata = @gzuncompress($data))
+								{
+									$data = $degzdata;
+									if($this->debug)
+									print "<PRE>---INFLATED RESPONSE---[".strlen($data)." chars]---\n" . htmlentities($data) . "\n---END---</PRE>";
+								}
+								elseif($GLOBALS['_xh']['headers']['content-encoding'] == 'gzip' && $degzdata = @gzinflate(substr($data, 10)))
+								{
+									$data = $degzdata;
+									if($this->debug)
+									print "<PRE>---INFLATED RESPONSE---[".strlen($data)." chars]---\n" . htmlentities($data) . "\n---END---</PRE>";
+								}
+								else
+								{
+									error_log('XML-RPC: '.__METHOD__.': errors occurred when trying to decode the deflated data received from server');
+									$r = new xmlrpcresp(0, $GLOBALS['xmlrpcerr']['decompress_fail'], $GLOBALS['xmlrpcstr']['decompress_fail']);
+									return $r;
+								}
+							}
+							else
+							{
+								error_log('XML-RPC: '.__METHOD__.': the server sent deflated data. Your php install must have the Zlib extension compiled in to support this.');
+								$r = new xmlrpcresp(0, $GLOBALS['xmlrpcerr']['cannot_decompress'], $GLOBALS['xmlrpcstr']['cannot_decompress']);
+								return $r;
+							}
+						}
+					}
+				} // end of 'if needed, de-chunk, re-inflate response'
+
+				// real stupid hack to avoid PHP complaining about returning NULL by ref
+				$r = null;
+				$r =& $r;
+				return $r;
+		}
+
+		/**
+		* Parse the xmlrpc response contained in the string $data and return an xmlrpcresp object.
+		* @param string $data the xmlrpc response, eventually including http headers
+		* @param bool $headers_processed when true prevents parsing HTTP headers for interpretation of content-encoding and consequent decoding
+		* @param string $return_type decides return type, i.e. content of response->value(). Either 'xmlrpcvals', 'xml' or 'phpvals'
+		* @return xmlrpcresp
+		* @access public
+		*/
+		function &parseResponse($data='', $headers_processed=false, $return_type='xmlrpcvals')
+		{
+			if($this->debug)
+			{
+				//by maHo, replaced htmlspecialchars with htmlentities
+				print "<PRE>---GOT---\n" . htmlentities($data) . "\n---END---\n</PRE>";
+			}
+
+			if($data == '')
+			{
+				error_log('XML-RPC: '.__METHOD__.': no response received from server.');
+				$r = new xmlrpcresp(0, $GLOBALS['xmlrpcerr']['no_data'], $GLOBALS['xmlrpcstr']['no_data']);
+				return $r;
+			}
+
+			$GLOBALS['_xh']=array();
+
+			$raw_data = $data;
+			// parse the HTTP headers of the response, if present, and separate them from data
+			if(substr($data, 0, 4) == 'HTTP')
+			{
+				$r =& $this->parseResponseHeaders($data, $headers_processed);
+				if ($r)
+				{
+					// failed processing of HTTP response headers
+					// save into response obj the full payload received, for debugging
+					$r->raw_data = $data;
+					return $r;
+				}
+			}
+			else
+			{
+				$GLOBALS['_xh']['headers'] = array();
+				$GLOBALS['_xh']['cookies'] = array();
+			}
+
+			if($this->debug)
+			{
+				$start = strpos($data, '<!-- SERVER DEBUG INFO (BASE64 ENCODED):');
+				if ($start)
+				{
+					$start += strlen('<!-- SERVER DEBUG INFO (BASE64 ENCODED):');
+					$end = strpos($data, '-->', $start);
+					$comments = substr($data, $start, $end-$start);
+					print "<PRE>---SERVER DEBUG INFO (DECODED) ---\n\t".htmlentities(str_replace("\n", "\n\t", base64_decode($comments)))."\n---END---\n</PRE>";
+				}
+			}
+
+			// be tolerant of extra whitespace in response body
+			$data = trim($data);
+
+			/// @todo return an error msg if $data=='' ?
+
+			// be tolerant of junk after methodResponse (e.g. javascript ads automatically inserted by free hosts)
+			// idea from Luca Mariano <[email protected]> originally in PEARified version of the lib
+			$pos = strrpos($data, '</methodResponse>');
+			if($pos !== false)
+			{
+				$data = substr($data, 0, $pos+17);
+			}
+
+			// if user wants back raw xml, give it to him
+			if ($return_type == 'xml')
+			{
+				$r = new xmlrpcresp($data, 0, '', 'xml');
+				$r->hdrs = $GLOBALS['_xh']['headers'];
+				$r->_cookies = $GLOBALS['_xh']['cookies'];
+				$r->raw_data = $raw_data;
+				return $r;
+			}
+
+			// try to 'guestimate' the character encoding of the received response
+			$resp_encoding = guess_encoding(@$GLOBALS['_xh']['headers']['content-type'], $data);
+
+			$GLOBALS['_xh']['ac']='';
+			//$GLOBALS['_xh']['qt']=''; //unused...
+			$GLOBALS['_xh']['stack'] = array();
+			$GLOBALS['_xh']['valuestack'] = array();
+			$GLOBALS['_xh']['isf']=0; // 0 = OK, 1 for xmlrpc fault responses, 2 = invalid xmlrpc
+			$GLOBALS['_xh']['isf_reason']='';
+			$GLOBALS['_xh']['rt']=''; // 'methodcall or 'methodresponse'
+
+			// if response charset encoding is not known / supported, try to use
+			// the default encoding and parse the xml anyway, but log a warning...
+			if (!in_array($resp_encoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII')))
+			// the following code might be better for mb_string enabled installs, but
+			// makes the lib about 200% slower...
+			//if (!is_valid_charset($resp_encoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII')))
+			{
+				error_log('XML-RPC: '.__METHOD__.': invalid charset encoding of received response: '.$resp_encoding);
+				$resp_encoding = $GLOBALS['xmlrpc_defencoding'];
+			}
+			$parser = xml_parser_create($resp_encoding);
+			xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, true);
+			// G. Giunta 2005/02/13: PHP internally uses ISO-8859-1, so we have to tell
+			// the xml parser to give us back data in the expected charset.
+			// What if internal encoding is not in one of the 3 allowed?
+			// we use the broadest one, ie. utf8
+			// This allows to send data which is native in various charset,
+			// by extending xmlrpc_encode_entitites() and setting xmlrpc_internalencoding
+			if (!in_array($GLOBALS['xmlrpc_internalencoding'], array('UTF-8', 'ISO-8859-1', 'US-ASCII')))
+			{
+				xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, 'UTF-8');
+			}
+			else
+			{
+				xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, $GLOBALS['xmlrpc_internalencoding']);
+			}
+
+			if ($return_type == 'phpvals')
+			{
+				xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_fast');
+			}
+			else
+			{
+				xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee');
+			}
+
+			xml_set_character_data_handler($parser, 'xmlrpc_cd');
+			xml_set_default_handler($parser, 'xmlrpc_dh');
+
+			// first error check: xml not well formed
+			if(!xml_parse($parser, $data, count($data)))
+			{
+				// thanks to Peter Kocks <[email protected]>
+				if((xml_get_current_line_number($parser)) == 1)
+				{
+					$errstr = 'XML error at line 1, check URL';
+				}
+				else
+				{
+					$errstr = sprintf('XML error: %s at line %d, column %d',
+						xml_error_string(xml_get_error_code($parser)),
+						xml_get_current_line_number($parser), xml_get_current_column_number($parser));
+				}
+				error_log($errstr);
+				$r=new xmlrpcresp(0, $GLOBALS['xmlrpcerr']['invalid_return'], $GLOBALS['xmlrpcstr']['invalid_return'].' ('.$errstr.')');
+				xml_parser_free($parser);
+				if($this->debug)
+				{
+					print $errstr;
+				}
+				$r->hdrs = $GLOBALS['_xh']['headers'];
+				$r->_cookies = $GLOBALS['_xh']['cookies'];
+				$r->raw_data = $raw_data;
+				return $r;
+			}
+			xml_parser_free($parser);
+			// second error check: xml well formed but not xml-rpc compliant
+			if ($GLOBALS['_xh']['isf'] > 1)
+			{
+				if ($this->debug)
+				{
+					/// @todo echo something for user?
+				}
+
+				$r = new xmlrpcresp(0, $GLOBALS['xmlrpcerr']['invalid_return'],
+				$GLOBALS['xmlrpcstr']['invalid_return'] . ' ' . $GLOBALS['_xh']['isf_reason']);
+			}
+			// third error check: parsing of the response has somehow gone boink.
+			// NB: shall we omit this check, since we trust the parsing code?
+			elseif ($return_type == 'xmlrpcvals' && !is_object($GLOBALS['_xh']['value']))
+			{
+				// something odd has happened
+				// and it's time to generate a client side error
+				// indicating something odd went on
+				$r=new xmlrpcresp(0, $GLOBALS['xmlrpcerr']['invalid_return'],
+					$GLOBALS['xmlrpcstr']['invalid_return']);
+			}
+			else
+			{
+				if ($this->debug)
+				{
+					print "<PRE>---PARSED---\n";
+					// somehow htmlentities chokes on var_export, and some full html string...
+					//print htmlentitites(var_export($GLOBALS['_xh']['value'], true));
+					print htmlspecialchars(var_export($GLOBALS['_xh']['value'], true));
+					print "\n---END---</PRE>";
+				}
+
+				// note that using =& will raise an error if $GLOBALS['_xh']['st'] does not generate an object.
+				$v =& $GLOBALS['_xh']['value'];
+
+				if($GLOBALS['_xh']['isf'])
+				{
+					/// @todo we should test here if server sent an int and a string,
+					/// and/or coerce them into such...
+					if ($return_type == 'xmlrpcvals')
+					{
+						$errno_v = $v->structmem('faultCode');
+						$errstr_v = $v->structmem('faultString');
+						$errno = $errno_v->scalarval();
+						$errstr = $errstr_v->scalarval();
+					}
+					else
+					{
+						$errno = $v['faultCode'];
+						$errstr = $v['faultString'];
+					}
+
+					if($errno == 0)
+					{
+						// FAULT returned, errno needs to reflect that
+						$errno = -1;
+					}
+
+					$r = new xmlrpcresp(0, $errno, $errstr);
+				}
+				else
+				{
+					$r=new xmlrpcresp($v, 0, '', $return_type);
+				}
+			}
+
+			$r->hdrs = $GLOBALS['_xh']['headers'];
+			$r->_cookies = $GLOBALS['_xh']['cookies'];
+			$r->raw_data = $raw_data;
+			return $r;
+		}
+	}
+
+	class xmlrpcval
+	{
+		var $me=array();
+		var $mytype=0;
+		var $_php_class=null;
+
+		/**
+		* @param mixed $val
+		* @param string $type any valid xmlrpc type name (lowercase). If null, 'string' is assumed
+		*/
+		function xmlrpcval($val=-1, $type='')
+		{
+			/// @todo: optimization creep - do not call addXX, do it all inline.
+			/// downside: booleans will not be coerced anymore
+			if($val!==-1 || $type!='')
+			{
+				// optimization creep: inlined all work done by constructor
+				switch($type)
+				{
+					case '':
+						$this->mytype=1;
+						$this->me['string']=$val;
+						break;
+					case 'i4':
+					case 'int':
+					case 'double':
+					case 'string':
+					case 'boolean':
+					case 'dateTime.iso8601':
+					case 'base64':
+					case 'null':
+						$this->mytype=1;
+						$this->me[$type]=$val;
+						break;
+					case 'array':
+						$this->mytype=2;
+						$this->me['array']=$val;
+						break;
+					case 'struct':
+						$this->mytype=3;
+						$this->me['struct']=$val;
+						break;
+					default:
+						error_log("XML-RPC: ".__METHOD__.": not a known type ($type)");
+				}
+				/*if($type=='')
+				{
+					$type='string';
+				}
+				if($GLOBALS['xmlrpcTypes'][$type]==1)
+				{
+					$this->addScalar($val,$type);
+				}
+				elseif($GLOBALS['xmlrpcTypes'][$type]==2)
+				{
+					$this->addArray($val);
+				}
+				elseif($GLOBALS['xmlrpcTypes'][$type]==3)
+				{
+					$this->addStruct($val);
+				}*/
+			}
+		}
+
+		/**
+		* Add a single php value to an (unitialized) xmlrpcval
+		* @param mixed $val
+		* @param string $type
+		* @return int 1 or 0 on failure
+		*/
+		function addScalar($val, $type='string')
+		{
+			$typeof=@$GLOBALS['xmlrpcTypes'][$type];
+			if($typeof!=1)
+			{
+				error_log("XML-RPC: ".__METHOD__.": not a scalar type ($type)");
+				return 0;
+			}
+
+			// coerce booleans into correct values
+			// NB: we should either do it for datetimes, integers and doubles, too,
+			// or just plain remove this check, implemented on booleans only...
+			if($type==$GLOBALS['xmlrpcBoolean'])
+			{
+				if(strcasecmp($val,'true')==0 || $val==1 || ($val==true && strcasecmp($val,'false')))
+				{
+					$val=true;
+				}
+				else
+				{
+					$val=false;
+				}
+			}
+
+			switch($this->mytype)
+			{
+				case 1:
+					error_log('XML-RPC: '.__METHOD__.': scalar xmlrpcval can have only one value');
+					return 0;
+				case 3:
+					error_log('XML-RPC: '.__METHOD__.': cannot add anonymous scalar to struct xmlrpcval');
+					return 0;
+				case 2:
+					// we're adding a scalar value to an array here
+					//$ar=$this->me['array'];
+					//$ar[]=new xmlrpcval($val, $type);
+					//$this->me['array']=$ar;
+					// Faster (?) avoid all the costly array-copy-by-val done here...
+					$this->me['array'][]=new xmlrpcval($val, $type);
+					return 1;
+				default:
+					// a scalar, so set the value and remember we're scalar
+					$this->me[$type]=$val;
+					$this->mytype=$typeof;
+					return 1;
+			}
+		}
+
+		/**
+		* Add an array of xmlrpcval objects to an xmlrpcval
+		* @param array $vals
+		* @return int 1 or 0 on failure
+		* @access public
+		*
+		* @todo add some checking for $vals to be an array of xmlrpcvals?
+		*/
+		function addArray($vals)
+		{
+			if($this->mytype==0)
+			{
+				$this->mytype=$GLOBALS['xmlrpcTypes']['array'];
+				$this->me['array']=$vals;
+				return 1;
+			}
+			elseif($this->mytype==2)
+			{
+				// we're adding to an array here
+				$this->me['array'] = array_merge($this->me['array'], $vals);
+				return 1;
+			}
+			else
+			{
+				error_log('XML-RPC: '.__METHOD__.': already initialized as a [' . $this->kindOf() . ']');
+				return 0;
+			}
+		}
+
+		/**
+		* Add an array of named xmlrpcval objects to an xmlrpcval
+		* @param array $vals
+		* @return int 1 or 0 on failure
+		* @access public
+		*
+		* @todo add some checking for $vals to be an array?
+		*/
+		function addStruct($vals)
+		{
+			if($this->mytype==0)
+			{
+				$this->mytype=$GLOBALS['xmlrpcTypes']['struct'];
+				$this->me['struct']=$vals;
+				return 1;
+			}
+			elseif($this->mytype==3)
+			{
+				// we're adding to a struct here
+				$this->me['struct'] = array_merge($this->me['struct'], $vals);
+				return 1;
+			}
+			else
+			{
+				error_log('XML-RPC: '.__METHOD__.': already initialized as a [' . $this->kindOf() . ']');
+				return 0;
+			}
+		}
+
+		// poor man's version of print_r ???
+		// DEPRECATED!
+		function dump($ar)
+		{
+			foreach($ar as $key => $val)
+			{
+				echo "$key => $val<br />";
+				if($key == 'array')
+				{
+					while(list($key2, $val2) = each($val))
+					{
+						echo "-- $key2 => $val2<br />";
+					}
+				}
+			}
+		}
+
+		/**
+		* Returns a string containing "struct", "array" or "scalar" describing the base type of the value
+		* @return string
+		* @access public
+		*/
+		function kindOf()
+		{
+			switch($this->mytype)
+			{
+				case 3:
+					return 'struct';
+					break;
+				case 2:
+					return 'array';
+					break;
+				case 1:
+					return 'scalar';
+					break;
+				default:
+					return 'undef';
+			}
+		}
+
+		/**
+		* @access private
+		*/
+		function serializedata($typ, $val, $charset_encoding='')
+		{
+			$rs='';
+			switch(@$GLOBALS['xmlrpcTypes'][$typ])
+			{
+				case 1:
+					switch($typ)
+					{
+						case $GLOBALS['xmlrpcBase64']:
+							$rs.="<${typ}>" . base64_encode($val) . "</${typ}>";
+							break;
+						case $GLOBALS['xmlrpcBoolean']:
+							$rs.="<${typ}>" . ($val ? '1' : '0') . "</${typ}>";
+							break;
+						case $GLOBALS['xmlrpcString']:
+							// G. Giunta 2005/2/13: do NOT use htmlentities, since
+							// it will produce named html entities, which are invalid xml
+							$rs.="<${typ}>" . xmlrpc_encode_entitites($val, $GLOBALS['xmlrpc_internalencoding'], $charset_encoding). "</${typ}>";
+							break;
+						case $GLOBALS['xmlrpcInt']:
+						case $GLOBALS['xmlrpcI4']:
+							$rs.="<${typ}>".(int)$val."</${typ}>";
+							break;
+						case $GLOBALS['xmlrpcDouble']:
+							// avoid using standard conversion of float to string because it is locale-dependent,
+							// and also because the xmlrpc spec forbids exponential notation.
+							// sprintf('%F') could be most likely ok but it fails eg. on 2e-14.
+							// The code below tries its best at keeping max precision while avoiding exp notation,
+							// but there is of course no limit in the number of decimal places to be used...
+							$rs.="<${typ}>".preg_replace('/\\.?0+$/','',number_format((double)$val, 128, '.', ''))."</${typ}>";
+							break;
+						case $GLOBALS['xmlrpcDateTime']:
+							if (is_string($val))
+							{
+								$rs.="<${typ}>${val}</${typ}>";
+							}
+							else if(is_a($val, 'DateTime'))
+							{
+								$rs.="<${typ}>".$val->format('Ymd\TH:i:s')."</${typ}>";
+							}
+							else if(is_int($val))
+							{
+								$rs.="<${typ}>".strftime("%Y%m%dT%H:%M:%S", $val)."</${typ}>";
+							}
+							else
+							{
+								// not really a good idea here: but what shall we output anyway? left for backward compat...
+								$rs.="<${typ}>${val}</${typ}>";
+							}
+							break;
+						case $GLOBALS['xmlrpcNull']:
+							if ($GLOBALS['xmlrpc_null_apache_encoding'])
+							{
+								$rs.="<ex:nil/>";
+							}
+							else
+							{
+								$rs.="<nil/>";
+							}
+							break;
+						default:
+							// no standard type value should arrive here, but provide a possibility
+							// for xmlrpcvals of unknown type...
+							$rs.="<${typ}>${val}</${typ}>";
+					}
+					break;
+				case 3:
+					// struct
+					if ($this->_php_class)
+					{
+						$rs.='<struct php_class="' . $this->_php_class . "\">\n";
+					}
+					else
+					{
+						$rs.="<struct>\n";
+					}
+					foreach($val as $key2 => $val2)
+					{
+						$rs.='<member><name>'.xmlrpc_encode_entitites($key2, $GLOBALS['xmlrpc_internalencoding'], $charset_encoding)."</name>\n";
+						//$rs.=$this->serializeval($val2);
+						$rs.=$val2->serialize($charset_encoding);
+						$rs.="</member>\n";
+					}
+					$rs.='</struct>';
+					break;
+				case 2:
+					// array
+					$rs.="<array>\n<data>\n";
+					for($i=0; $i<count($val); $i++)
+					{
+						//$rs.=$this->serializeval($val[$i]);
+						$rs.=$val[$i]->serialize($charset_encoding);
+					}
+					$rs.="</data>\n</array>";
+					break;
+				default:
+					break;
+			}
+			return $rs;
+		}
+
+		/**
+		* Returns xml representation of the value. XML prologue not included
+		* @param string $charset_encoding the charset to be used for serialization. if null, US-ASCII is assumed
+		* @return string
+		* @access public
+		*/
+		function serialize($charset_encoding='')
+		{
+			// add check? slower, but helps to avoid recursion in serializing broken xmlrpcvals...
+			//if (is_object($o) && (get_class($o) == 'xmlrpcval' || is_subclass_of($o, 'xmlrpcval')))
+			//{
+				reset($this->me);
+				list($typ, $val) = each($this->me);
+				return '<value>' . $this->serializedata($typ, $val, $charset_encoding) . "</value>\n";
+			//}
+		}
+
+		// DEPRECATED
+		function serializeval($o)
+		{
+			// add check? slower, but helps to avoid recursion in serializing broken xmlrpcvals...
+			//if (is_object($o) && (get_class($o) == 'xmlrpcval' || is_subclass_of($o, 'xmlrpcval')))
+			//{
+				$ar=$o->me;
+				reset($ar);
+				list($typ, $val) = each($ar);
+				return '<value>' . $this->serializedata($typ, $val) . "</value>\n";
+			//}
+		}
+
+		/**
+		* Checks wheter a struct member with a given name is present.
+		* Works only on xmlrpcvals of type struct.
+		* @param string $m the name of the struct member to be looked up
+		* @return boolean
+		* @access public
+		*/
+		function structmemexists($m)
+		{
+			return array_key_exists($m, $this->me['struct']);
+		}
+
+		/**
+		* Returns the value of a given struct member (an xmlrpcval object in itself).
+		* Will raise a php warning if struct member of given name does not exist
+		* @param string $m the name of the struct member to be looked up
+		* @return xmlrpcval
+		* @access public
+		*/
+		function structmem($m)
+		{
+			return $this->me['struct'][$m];
+		}
+
+		/**
+		* Reset internal pointer for xmlrpcvals of type struct.
+		* @access public
+		*/
+		function structreset()
+		{
+			reset($this->me['struct']);
+		}
+
+		/**
+		* Return next member element for xmlrpcvals of type struct.
+		* @return xmlrpcval
+		* @access public
+		*/
+		function structeach()
+		{
+			return each($this->me['struct']);
+		}
+
+		// DEPRECATED! this code looks like it is very fragile and has not been fixed
+		// for a long long time. Shall we remove it for 2.0?
+		function getval()
+		{
+			// UNSTABLE
+			reset($this->me);
+			list($a,$b)=each($this->me);
+			// contributed by I Sofer, 2001-03-24
+			// add support for nested arrays to scalarval
+			// i've created a new method here, so as to
+			// preserve back compatibility
+
+			if(is_array($b))
+			{
+				@reset($b);
+				while(list($id,$cont) = @each($b))
+				{
+					$b[$id] = $cont->scalarval();
+				}
+			}
+
+			// add support for structures directly encoding php objects
+			if(is_object($b))
+			{
+				$t = get_object_vars($b);
+				@reset($t);
+				while(list($id,$cont) = @each($t))
+				{
+					$t[$id] = $cont->scalarval();
+				}
+				@reset($t);
+				while(list($id,$cont) = @each($t))
+				{
+					@$b->$id = $cont;
+				}
+			}
+			// end contrib
+			return $b;
+		}
+
+		/**
+		* Returns the value of a scalar xmlrpcval
+		* @return mixed
+		* @access public
+		*/
+		function scalarval()
+		{
+			reset($this->me);
+			list(,$b)=each($this->me);
+			return $b;
+		}
+
+		/**
+		* Returns the type of the xmlrpcval.
+		* For integers, 'int' is always returned in place of 'i4'
+		* @return string
+		* @access public
+		*/
+		function scalartyp()
+		{
+			reset($this->me);
+			list($a,)=each($this->me);
+			if($a==$GLOBALS['xmlrpcI4'])
+			{
+				$a=$GLOBALS['xmlrpcInt'];
+			}
+			return $a;
+		}
+
+		/**
+		* Returns the m-th member of an xmlrpcval of struct type
+		* @param integer $m the index of the value to be retrieved (zero based)
+		* @return xmlrpcval
+		* @access public
+		*/
+		function arraymem($m)
+		{
+			return $this->me['array'][$m];
+		}
+
+		/**
+		* Returns the number of members in an xmlrpcval of array type
+		* @return integer
+		* @access public
+		*/
+		function arraysize()
+		{
+			return count($this->me['array']);
+		}
+
+		/**
+		* Returns the number of members in an xmlrpcval of struct type
+		* @return integer
+		* @access public
+		*/
+		function structsize()
+		{
+			return count($this->me['struct']);
+		}
+	}
+
+
+	// date helpers
+
+	/**
+	* Given a timestamp, return the corresponding ISO8601 encoded string.
+	*
+	* Really, timezones ought to be supported
+	* but the XML-RPC spec says:
+	*
+	* "Don't assume a timezone. It should be specified by the server in its
+	* documentation what assumptions it makes about timezones."
+	*
+	* These routines always assume localtime unless
+	* $utc is set to 1, in which case UTC is assumed
+	* and an adjustment for locale is made when encoding
+	*
+	* @param int $timet (timestamp)
+	* @param int $utc (0 or 1)
+	* @return string
+	*/
+	function iso8601_encode($timet, $utc=0)
+	{
+		if(!$utc)
+		{
+			$t=strftime("%Y%m%dT%H:%M:%S", $timet);
+		}
+		else
+		{
+			if(function_exists('gmstrftime'))
+			{
+				// gmstrftime doesn't exist in some versions
+				// of PHP
+				$t=gmstrftime("%Y%m%dT%H:%M:%S", $timet);
+			}
+			else
+			{
+				$t=strftime("%Y%m%dT%H:%M:%S", $timet-date('Z'));
+			}
+		}
+		return $t;
+	}
+
+	/**
+	* Given an ISO8601 date string, return a timet in the localtime, or UTC
+	* @param string $idate
+	* @param int $utc either 0 or 1
+	* @return int (datetime)
+	*/
+	function iso8601_decode($idate, $utc=0)
+	{
+		$t=0;
+		if(preg_match('/([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})/', $idate, $regs))
+		{
+			if($utc)
+			{
+				$t=gmmktime($regs[4], $regs[5], $regs[6], $regs[2], $regs[3], $regs[1]);
+			}
+			else
+			{
+				$t=mktime($regs[4], $regs[5], $regs[6], $regs[2], $regs[3], $regs[1]);
+			}
+		}
+		return $t;
+	}
+
+	/**
+	* Takes an xmlrpc value in PHP xmlrpcval object format and translates it into native PHP types.
+	*
+	* Works with xmlrpc message objects as input, too.
+	*
+	* Given proper options parameter, can rebuild generic php object instances
+	* (provided those have been encoded to xmlrpc format using a corresponding
+	* option in php_xmlrpc_encode())
+	* PLEASE NOTE that rebuilding php objects involves calling their constructor function.
+	* This means that the remote communication end can decide which php code will
+	* get executed on your server, leaving the door possibly open to 'php-injection'
+	* style of attacks (provided you have some classes defined on your server that
+	* might wreak havoc if instances are built outside an appropriate context).
+	* Make sure you trust the remote server/client before eanbling this!
+	*
+	* @author Dan Libby ([email protected])
+	*
+	* @param xmlrpcval $xmlrpc_val
+	* @param array $options if 'decode_php_objs' is set in the options array, xmlrpc structs can be decoded into php objects; if 'dates_as_objects' is set xmlrpc datetimes are decoded as php DateTime objects (standard is
+	* @return mixed
+	*/
+	function php_xmlrpc_decode($xmlrpc_val, $options=array())
+	{
+		switch($xmlrpc_val->kindOf())
+		{
+			case 'scalar':
+				if (in_array('extension_api', $options))
+				{
+					reset($xmlrpc_val->me);
+					list($typ,$val) = each($xmlrpc_val->me);
+					switch ($typ)
+					{
+						case 'dateTime.iso8601':
+							$xmlrpc_val->scalar = $val;
+							$xmlrpc_val->xmlrpc_type = 'datetime';
+							$xmlrpc_val->timestamp = iso8601_decode($val);
+							return $xmlrpc_val;
+						case 'base64':
+							$xmlrpc_val->scalar = $val;
+							$xmlrpc_val->type = $typ;
+							return $xmlrpc_val;
+						default:
+							return $xmlrpc_val->scalarval();
+					}
+				}
+				if (in_array('dates_as_objects', $options) && $xmlrpc_val->scalartyp() == 'dateTime.iso8601')
+				{
+					// we return a Datetime object instead of a string
+					// since now the constructor of xmlrpcval accepts safely strings, ints and datetimes,
+					// we cater to all 3 cases here
+					$out = $xmlrpc_val->scalarval();
+					if (is_string($out))
+					{
+						$out = strtotime($out);
+					}
+					if (is_int($out))
+					{
+						$result = new Datetime();
+						$result->setTimestamp($out);
+						return $result;
+					}
+					elseif (is_a($out, 'Datetime'))
+					{
+						return $out;
+					}
+				}
+				return $xmlrpc_val->scalarval();
+			case 'array':
+				$size = $xmlrpc_val->arraysize();
+				$arr = array();
+				for($i = 0; $i < $size; $i++)
+				{
+					$arr[] = php_xmlrpc_decode($xmlrpc_val->arraymem($i), $options);
+				}
+				return $arr;
+			case 'struct':
+				$xmlrpc_val->structreset();
+				// If user said so, try to rebuild php objects for specific struct vals.
+				/// @todo should we raise a warning for class not found?
+				// shall we check for proper subclass of xmlrpcval instead of
+				// presence of _php_class to detect what we can do?
+				if (in_array('decode_php_objs', $options) && $xmlrpc_val->_php_class != ''
+					&& class_exists($xmlrpc_val->_php_class))
+				{
+					$obj = @new $xmlrpc_val->_php_class;
+					while(list($key,$value)=$xmlrpc_val->structeach())
+					{
+						$obj->$key = php_xmlrpc_decode($value, $options);
+					}
+					return $obj;
+				}
+				else
+				{
+					$arr = array();
+					while(list($key,$value)=$xmlrpc_val->structeach())
+					{
+						$arr[$key] = php_xmlrpc_decode($value, $options);
+					}
+					return $arr;
+				}
+			case 'msg':
+				$paramcount = $xmlrpc_val->getNumParams();
+				$arr = array();
+				for($i = 0; $i < $paramcount; $i++)
+				{
+					$arr[] = php_xmlrpc_decode($xmlrpc_val->getParam($i));
+				}
+				return $arr;
+			}
+	}
+
+	// This constant left here only for historical reasons...
+	// it was used to decide if we have to define xmlrpc_encode on our own, but
+	// we do not do it anymore
+	if(function_exists('xmlrpc_decode'))
+	{
+		define('XMLRPC_EPI_ENABLED','1');
+	}
+	else
+	{
+		define('XMLRPC_EPI_ENABLED','0');
+	}
+
+	/**
+	* Takes native php types and encodes them into xmlrpc PHP object format.
+	* It will not re-encode xmlrpcval objects.
+	*
+	* Feature creep -- could support more types via optional type argument
+	* (string => datetime support has been added, ??? => base64 not yet)
+	*
+	* If given a proper options parameter, php object instances will be encoded
+	* into 'special' xmlrpc values, that can later be decoded into php objects
+	* by calling php_xmlrpc_decode() with a corresponding option
+	*
+	* @author Dan Libby ([email protected])
+	*
+	* @param mixed $php_val the value to be converted into an xmlrpcval object
+	* @param array $options	can include 'encode_php_objs', 'auto_dates', 'null_extension' or 'extension_api'
+	* @return xmlrpcval
+	*/
+	function php_xmlrpc_encode($php_val, $options=array())
+	{
+		$type = gettype($php_val);
+		switch($type)
+		{
+			case 'string':
+				if (in_array('auto_dates', $options) && preg_match('/^[0-9]{8}T[0-9]{2}:[0-9]{2}:[0-9]{2}$/', $php_val))
+					$xmlrpc_val = new xmlrpcval($php_val, $GLOBALS['xmlrpcDateTime']);
+				else
+					$xmlrpc_val = new xmlrpcval($php_val, $GLOBALS['xmlrpcString']);
+				break;
+			case 'integer':
+				$xmlrpc_val = new xmlrpcval($php_val, $GLOBALS['xmlrpcInt']);
+				break;
+			case 'double':
+				$xmlrpc_val = new xmlrpcval($php_val, $GLOBALS['xmlrpcDouble']);
+				break;
+				// <G_Giunta_2001-02-29>
+				// Add support for encoding/decoding of booleans, since they are supported in PHP
+			case 'boolean':
+				$xmlrpc_val = new xmlrpcval($php_val, $GLOBALS['xmlrpcBoolean']);
+				break;
+				// </G_Giunta_2001-02-29>
+			case 'array':
+				// PHP arrays can be encoded to either xmlrpc structs or arrays,
+				// depending on wheter they are hashes or plain 0..n integer indexed
+				// A shorter one-liner would be
+				// $tmp = array_diff(array_keys($php_val), range(0, count($php_val)-1));
+				// but execution time skyrockets!
+				$j = 0;
+				$arr = array();
+				$ko = false;
+				foreach($php_val as $key => $val)
+				{
+					$arr[$key] = php_xmlrpc_encode($val, $options);
+					if(!$ko && $key !== $j)
+					{
+						$ko = true;
+					}
+					$j++;
+				}
+				if($ko)
+				{
+					$xmlrpc_val = new xmlrpcval($arr, $GLOBALS['xmlrpcStruct']);
+				}
+				else
+				{
+					$xmlrpc_val = new xmlrpcval($arr, $GLOBALS['xmlrpcArray']);
+				}
+				break;
+			case 'object':
+				if(is_a($php_val, 'xmlrpcval'))
+				{
+					$xmlrpc_val = $php_val;
+				}
+				else if(is_a($php_val, 'DateTime'))
+				{
+					$xmlrpc_val = new xmlrpcval($php_val->format('Ymd\TH:i:s'), $GLOBALS['xmlrpcStruct']);
+				}
+				else
+				{
+					$arr = array();
+					reset($php_val);
+					while(list($k,$v) = each($php_val))
+					{
+						$arr[$k] = php_xmlrpc_encode($v, $options);
+					}
+					$xmlrpc_val = new xmlrpcval($arr, $GLOBALS['xmlrpcStruct']);
+					if (in_array('encode_php_objs', $options))
+					{
+						// let's save original class name into xmlrpcval:
+						// might be useful later on...
+						$xmlrpc_val->_php_class = get_class($php_val);
+					}
+				}
+				break;
+			case 'NULL':
+				if (in_array('extension_api', $options))
+				{
+					$xmlrpc_val = new xmlrpcval('', $GLOBALS['xmlrpcString']);
+				}
+				else if (in_array('null_extension', $options))
+				{
+					$xmlrpc_val = new xmlrpcval('', $GLOBALS['xmlrpcNull']);
+				}
+				else
+				{
+					$xmlrpc_val = new xmlrpcval();
+				}
+				break;
+			case 'resource':
+				if (in_array('extension_api', $options))
+				{
+					$xmlrpc_val = new xmlrpcval((int)$php_val, $GLOBALS['xmlrpcInt']);
+				}
+				else
+				{
+					$xmlrpc_val = new xmlrpcval();
+				}
+			// catch "user function", "unknown type"
+			default:
+				// giancarlo pinerolo <[email protected]>
+				// it has to return
+				// an empty object in case, not a boolean.
+				$xmlrpc_val = new xmlrpcval();
+				break;
+			}
+			return $xmlrpc_val;
+	}
+
+	/**
+	* Convert the xml representation of a method response, method request or single
+	* xmlrpc value into the appropriate object (a.k.a. deserialize)
+	* @param string $xml_val
+	* @param array $options
+	* @return mixed false on error, or an instance of either xmlrpcval, xmlrpcmsg or xmlrpcresp
+	*/
+	function php_xmlrpc_decode_xml($xml_val, $options=array())
+	{
+		$GLOBALS['_xh'] = array();
+		$GLOBALS['_xh']['ac'] = '';
+		$GLOBALS['_xh']['stack'] = array();
+		$GLOBALS['_xh']['valuestack'] = array();
+		$GLOBALS['_xh']['params'] = array();
+		$GLOBALS['_xh']['pt'] = array();
+		$GLOBALS['_xh']['isf'] = 0;
+		$GLOBALS['_xh']['isf_reason'] = '';
+		$GLOBALS['_xh']['method'] = false;
+		$GLOBALS['_xh']['rt'] = '';
+		/// @todo 'guestimate' encoding
+		$parser = xml_parser_create();
+		xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, true);
+		// What if internal encoding is not in one of the 3 allowed?
+		// we use the broadest one, ie. utf8!
+		if (!in_array($GLOBALS['xmlrpc_internalencoding'], array('UTF-8', 'ISO-8859-1', 'US-ASCII')))
+		{
+			xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, 'UTF-8');
+		}
+		else
+		{
+			xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, $GLOBALS['xmlrpc_internalencoding']);
+		}
+		xml_set_element_handler($parser, 'xmlrpc_se_any', 'xmlrpc_ee');
+		xml_set_character_data_handler($parser, 'xmlrpc_cd');
+		xml_set_default_handler($parser, 'xmlrpc_dh');
+		if(!xml_parse($parser, $xml_val, 1))
+		{
+			$errstr = sprintf('XML error: %s at line %d, column %d',
+						xml_error_string(xml_get_error_code($parser)),
+						xml_get_current_line_number($parser), xml_get_current_column_number($parser));
+			error_log($errstr);
+			xml_parser_free($parser);
+			return false;
+		}
+		xml_parser_free($parser);
+		if ($GLOBALS['_xh']['isf'] > 1) // test that $GLOBALS['_xh']['value'] is an obj, too???
+		{
+			error_log($GLOBALS['_xh']['isf_reason']);
+			return false;
+		}
+		switch ($GLOBALS['_xh']['rt'])
+		{
+			case 'methodresponse':
+				$v =& $GLOBALS['_xh']['value'];
+				if ($GLOBALS['_xh']['isf'] == 1)
+				{
+					$vc = $v->structmem('faultCode');
+					$vs = $v->structmem('faultString');
+					$r = new xmlrpcresp(0, $vc->scalarval(), $vs->scalarval());
+				}
+				else
+				{
+					$r = new xmlrpcresp($v);
+				}
+				return $r;
+			case 'methodcall':
+				$m = new xmlrpcmsg($GLOBALS['_xh']['method']);
+				for($i=0; $i < count($GLOBALS['_xh']['params']); $i++)
+				{
+					$m->addParam($GLOBALS['_xh']['params'][$i]);
+				}
+				return $m;
+			case 'value':
+				return $GLOBALS['_xh']['value'];
+			default:
+				return false;
+		}
+	}
+
+	/**
+	* decode a string that is encoded w/ "chunked" transfer encoding
+	* as defined in rfc2068 par. 19.4.6
+	* code shamelessly stolen from nusoap library by Dietrich Ayala
+	*
+	* @param string $buffer the string to be decoded
+	* @return string
+	*/
+	function decode_chunked($buffer)
+	{
+		// length := 0
+		$length = 0;
+		$new = '';
+
+		// read chunk-size, chunk-extension (if any) and crlf
+		// get the position of the linebreak
+		$chunkend = strpos($buffer,"\r\n") + 2;
+		$temp = substr($buffer,0,$chunkend);
+		$chunk_size = hexdec( trim($temp) );
+		$chunkstart = $chunkend;
+		while($chunk_size > 0)
+		{
+			$chunkend = strpos($buffer, "\r\n", $chunkstart + $chunk_size);
+
+			// just in case we got a broken connection
+			if($chunkend == false)
+			{
+				$chunk = substr($buffer,$chunkstart);
+				// append chunk-data to entity-body
+				$new .= $chunk;
+				$length += strlen($chunk);
+				break;
+			}
+
+			// read chunk-data and crlf
+			$chunk = substr($buffer,$chunkstart,$chunkend-$chunkstart);
+			// append chunk-data to entity-body
+			$new .= $chunk;
+			// length := length + chunk-size
+			$length += strlen($chunk);
+			// read chunk-size and crlf
+			$chunkstart = $chunkend + 2;
+
+			$chunkend = strpos($buffer,"\r\n",$chunkstart)+2;
+			if($chunkend == false)
+			{
+				break; //just in case we got a broken connection
+			}
+			$temp = substr($buffer,$chunkstart,$chunkend-$chunkstart);
+			$chunk_size = hexdec( trim($temp) );
+			$chunkstart = $chunkend;
+		}
+		return $new;
+	}
+
+	/**
+	* xml charset encoding guessing helper function.
+	* Tries to determine the charset encoding of an XML chunk received over HTTP.
+	* NB: according to the spec (RFC 3023), if text/xml content-type is received over HTTP without a content-type,
+	* we SHOULD assume it is strictly US-ASCII. But we try to be more tolerant of unconforming (legacy?) clients/servers,
+	* which will be most probably using UTF-8 anyway...
+	*
+	* @param string $httpheaders the http Content-type header
+	* @param string $xmlchunk xml content buffer
+	* @param string $encoding_prefs comma separated list of character encodings to be used as default (when mb extension is enabled)
+	*
+	* @todo explore usage of mb_http_input(): does it detect http headers + post data? if so, use it instead of hand-detection!!!
+	*/
+	function guess_encoding($httpheader='', $xmlchunk='', $encoding_prefs=null)
+	{
+		// discussion: see http://www.yale.edu/pclt/encoding/
+		// 1 - test if encoding is specified in HTTP HEADERS
+
+		//Details:
+		// LWS:           (\13\10)?( |\t)+
+		// token:         (any char but excluded stuff)+
+		// quoted string: " (any char but double quotes and cointrol chars)* "
+		// header:        Content-type = ...; charset=value(; ...)*
+		//   where value is of type token, no LWS allowed between 'charset' and value
+		// Note: we do not check for invalid chars in VALUE:
+		//   this had better be done using pure ereg as below
+		// Note 2: we might be removing whitespace/tabs that ought to be left in if
+		//   the received charset is a quoted string. But nobody uses such charset names...
+
+		/// @todo this test will pass if ANY header has charset specification, not only Content-Type. Fix it?
+		$matches = array();
+		if(preg_match('/;\s*charset\s*=([^;]+)/i', $httpheader, $matches))
+		{
+			return strtoupper(trim($matches[1], " \t\""));
+		}
+
+		// 2 - scan the first bytes of the data for a UTF-16 (or other) BOM pattern
+		//     (source: http://www.w3.org/TR/2000/REC-xml-20001006)
+		//     NOTE: actually, according to the spec, even if we find the BOM and determine
+		//     an encoding, we should check if there is an encoding specified
+		//     in the xml declaration, and verify if they match.
+		/// @todo implement check as described above?
+		/// @todo implement check for first bytes of string even without a BOM? (It sure looks harder than for cases WITH a BOM)
+		if(preg_match('/^(\x00\x00\xFE\xFF|\xFF\xFE\x00\x00|\x00\x00\xFF\xFE|\xFE\xFF\x00\x00)/', $xmlchunk))
+		{
+			return 'UCS-4';
+		}
+		elseif(preg_match('/^(\xFE\xFF|\xFF\xFE)/', $xmlchunk))
+		{
+			return 'UTF-16';
+		}
+		elseif(preg_match('/^(\xEF\xBB\xBF)/', $xmlchunk))
+		{
+			return 'UTF-8';
+		}
+
+		// 3 - test if encoding is specified in the xml declaration
+		// Details:
+		// SPACE:         (#x20 | #x9 | #xD | #xA)+ === [ \x9\xD\xA]+
+		// EQ:            SPACE?=SPACE? === [ \x9\xD\xA]*=[ \x9\xD\xA]*
+		if (preg_match('/^<\?xml\s+version\s*=\s*'. "((?:\"[a-zA-Z0-9_.:-]+\")|(?:'[a-zA-Z0-9_.:-]+'))".
+			'\s+encoding\s*=\s*' . "((?:\"[A-Za-z][A-Za-z0-9._-]*\")|(?:'[A-Za-z][A-Za-z0-9._-]*'))/",
+			$xmlchunk, $matches))
+		{
+			return strtoupper(substr($matches[2], 1, -1));
+		}
+
+		// 4 - if mbstring is available, let it do the guesswork
+		// NB: we favour finding an encoding that is compatible with what we can process
+		if(extension_loaded('mbstring'))
+		{
+			if($encoding_prefs)
+			{
+				$enc = mb_detect_encoding($xmlchunk, $encoding_prefs);
+			}
+			else
+			{
+				$enc = mb_detect_encoding($xmlchunk);
+			}
+			// NB: mb_detect likes to call it ascii, xml parser likes to call it US_ASCII...
+			// IANA also likes better US-ASCII, so go with it
+			if($enc == 'ASCII')
+			{
+				$enc = 'US-'.$enc;
+			}
+			return $enc;
+		}
+		else
+		{
+			// no encoding specified: as per HTTP1.1 assume it is iso-8859-1?
+			// Both RFC 2616 (HTTP 1.1) and 1945 (HTTP 1.0) clearly state that for text/xxx content types
+			// this should be the standard. And we should be getting text/xml as request and response.
+			// BUT we have to be backward compatible with the lib, which always used UTF-8 as default...
+			return $GLOBALS['xmlrpc_defencoding'];
+		}
+	}
+
+	/**
+	* Checks if a given charset encoding is present in a list of encodings or
+	* if it is a valid subset of any encoding in the list
+	* @param string $encoding charset to be tested
+	* @param mixed $validlist comma separated list of valid charsets (or array of charsets)
+	*/
+	function is_valid_charset($encoding, $validlist)
+	{
+		$charset_supersets = array(
+			'US-ASCII' => array ('ISO-8859-1', 'ISO-8859-2', 'ISO-8859-3', 'ISO-8859-4',
+				'ISO-8859-5', 'ISO-8859-6', 'ISO-8859-7', 'ISO-8859-8',
+				'ISO-8859-9', 'ISO-8859-10', 'ISO-8859-11', 'ISO-8859-12',
+				'ISO-8859-13', 'ISO-8859-14', 'ISO-8859-15', 'UTF-8',
+				'EUC-JP', 'EUC-', 'EUC-KR', 'EUC-CN')
+		);
+		if (is_string($validlist))
+			$validlist = explode(',', $validlist);
+		if (@in_array(strtoupper($encoding), $validlist))
+			return true;
+		else
+		{
+			if (array_key_exists($encoding, $charset_supersets))
+				foreach ($validlist as $allowed)
+					if (in_array($allowed, $charset_supersets[$encoding]))
+						return true;
+				return false;
+		}
+	}
+
+?>

File diff suppressed because it is too large
+ 3 - 0
site/Modernizr.js


+ 322 - 0
site/api/index.php

@@ -0,0 +1,322 @@
+<?php
+	header('Content-type: application/json');
+	header('Access-Control-Allow-Origin: *');
+	require_once("../../header.php");
+	if(!isset($_GET['action'])){
+		$opts = getopt('a:',Array('action:'));
+		$_GET['action'] = isset($opts['action'])?$opts['action']:(isset($opts['a'])?$opts['a']:'');
+	}
+	$u = is_logged_in();
+	switch($_GET['action']){
+		case 'test':
+			//$u or die();
+			//print_r(atheme_command(get_conf('xmlrpc-server'),get_conf('xmlrpc-port'),get_conf('xmlrpc-path'),USER_IP,$_COOKIE['user'],$_SESSION['password'],'topic','#omnimaga'));
+			echo mkpasswd('root');
+			die();
+		break;
+		case 'login':
+			isset($_GET['username']) && isset($_GET['password']) or die('{"code":2,"message":"Missing username and/or password"}');
+			isset($_GET['type']) or die('{"code":2,"message":"Missing user type"}');
+			$r = login($_GET['username'],$_GET['password'],$_GET['type']);
+			if($r !== true){
+				die('{"code":2,"message":"'.$r.'"}');
+			}else{
+				die('{"code":0}');
+			}
+		break;
+		case 'verify':
+			isset($_GET['token']) or die('{"code":1,"message":"No token set"}');
+			$r = verify($_GET['token']);
+			if($r !== true){
+				die('{"code":2,"message":"'.$r.'"}');
+			}
+			die('{"code":0,"message":"'.$r.'"}');
+		break;
+		case 'logout':
+			logout();
+			die('{"code":0}');
+		break;
+		case 'get-memos':
+			$u or die('{"code":1,"message":"You have been logged out"}');
+			$u['type'] = 'user' && isset($_COOKIE['user']) && isset($_SESSION['password']) or die('{"code":0}');
+			$res = atheme_command(get_conf('xmlrpc-server'),get_conf('xmlrpc-port'),get_conf('xmlrpc-path'),USER_IP,$_COOKIE['user'],$_SESSION['password'],'MemoServ','list');
+			if($res[0]){
+				$res = explode('&#10;',$res[1]);
+				$memos = Array();
+				foreach($res as $k => $row){
+					if($k != 0 && $k != 1){
+						$row = preg_split('/^-\s/',$row);
+						if(isset($row[1])){
+							$row = explode(' ',$row[1]);
+							$memo = atheme_command(get_conf('xmlrpc-server'),get_conf('xmlrpc-port'),get_conf('xmlrpc-path'),USER_IP,$_COOKIE['user'],$_SESSION['password'],'MemoServ','read',Array($row[0]));
+							$memo = explode('&#10;',$memo[1]);
+							array_push($memos,Array(
+								'id'=>$row[0],
+								'from'=>$row[2],
+								'date'=>Array(
+									'month'=>$row[4],
+									'day'=>$row[5],
+									'time'=>$row[6],
+									'year'=>$row[7]
+								),
+								'body'=>$memo[2]
+							));
+						}
+					}
+				}
+				die('{"code":0,"memos":'.json_encode($memos).'}');
+			}else{
+				die('{"code":1,"message":"Cannot fetch memos"}');
+			}
+		break;
+		case 'get-news':
+			$u or die('{"code":1,"message":"You have been logged out"}');
+			$u['type'] = 'user' && isset($_COOKIE['user']) && isset($_SESSION['password']) or die('{"code":0}');
+			$res = atheme_command(get_conf('xmlrpc-server'),get_conf('xmlrpc-port'),get_conf('xmlrpc-path'),USER_IP,$_COOKIE['user'],$_SESSION['password'],'InfoServ','list');
+			if($res[0]){
+				$res = explode('&#10;',$res[1]);
+				$news = Array();
+				foreach($res as $k => $row){
+					if($k != count($res)-1){
+						array_push($news,Array(
+							'id'=>preg_replace('/^(\d)+:.+$/i','\1',$row),
+							'title'=>preg_replace('/^\d+: \[(.+)\] .+/i','\1',$row),
+							'from'=>preg_replace('/^\d+: \[.+\] by (.+) at \d\d?:\d\d? on (\d\d)\/\d\d\/\d\d\d\d: .+/i','\1',$row),
+							'date'=>Array(
+								'time'=>preg_replace('/^\d+: \[.+\] by .+ at (\d\d?:\d\d?) on .+/','\1',$row),
+								'day'=>preg_replace('/^\d+: \[.+\] by .+ at \d\d?:\d\d? on (\d\d)\/\d\d\/\d\d\d\d: .+/i','\1',$row),
+								'month'=>preg_replace('/^\d+: \[.+\] by .+ at \d\d?:\d\d? on \d\d\/(\d\d)\/\d\d\d\d: .+/i','\1',$row),
+								'year'=>preg_replace('/^\d+: \[.+\] by .+ at \d\d?:\d\d? on \d\d\/\d\d\/(\d\d\d\d): .+/i','\1',$row)
+							),
+							'body'=>preg_replace('/^\d+: \[.+\] by .+ at \d\d?:\d\d? on \d\d\/\d\d\/\d\d\d\d: (.+)/i','\1',$row)
+						));
+					}
+				}
+				die('{"code":0,"news":'.json_encode($news).'}');
+			}else{
+				die('{"code":1,"message":"Cannot fetch news"}');
+			}
+		break;
+		case 'get-channels':
+			$u or die('{"code":1,"message":"You have been logged out"}');
+			$u['type'] = 'user' && isset($_COOKIE['user']) && isset($_SESSION['password']) or die('{"code":0}');
+			$res = atheme_command(get_conf('xmlrpc-server'),get_conf('xmlrpc-port'),get_conf('xmlrpc-path'),USER_IP,$_COOKIE['user'],$_SESSION['password'],'NickServ','listchans');
+			if($res[0]){
+				$res = explode('&#10;',$res[1]);
+				$channels = Array();
+				foreach($res as $k => $row){
+					if($k != count($res)-1){
+						$flags_list = str_split(preg_replace('/^Access flag\(s\) \+(.+) in .+$/i','\1',$row));
+						$flags = array();
+						foreach($flags_list as $kk => $flag){
+							switch($flag){
+								case 'v':$name='Voice';break;
+								case 'V':$name='Automatic voice';break;
+								case 'h':$name='Halfop';break;
+								case 'H':$name='Automatic Halfop';break;
+								case 'o':$name='Op';break;
+								case 'O':$name='Automatic Op';break;
+								case 'a':$name='Admin';break;
+								case 'q':$name='Owner';break;
+								case 's':$name='Set';break;
+								case 'i':$name='Invite/Getkey';break;
+								case 'r':$name='Kick/Ban';break;
+								case 'R':$name='Recover/Clear';break;
+								case 'f':$name='Modify access lists';break;
+								case 't':$name='Topic';break;
+								case 'A':$name='View access lists';break;
+								case 'F':$name='Founder';break;
+								case 'b':$name='Banned';break;
+								default:$name=$flag;
+							}
+							array_push($flags,array(
+								'flag'=>$flag,
+								'name'=>$name
+							));
+						}
+						$name = preg_replace('/^Access flag\(s\) \+.+ in (.+)$/i','\1',$row);
+						array_push($channels,Array(
+							'name'=>$name,
+							'flags'=>$flags
+						));
+					}
+				}
+				die('{"code":0,"channels":'.json_encode($channels).'}');
+			}else{
+				die('{"code":1,"message":"Cannot fetch channels"}');
+			}
+		break;
+		case 'send-memo':
+			$u or die('{"code":1,"message":"You have been logged out"}');
+			isset($_GET['to']) && isset($_GET['message']) or die('{"code":1,"message":"No message or user entered"}');
+			$res = atheme_command(get_conf('xmlrpc-server'),get_conf('xmlrpc-port'),get_conf('xmlrpc-path'),USER_IP,$_COOKIE['user'],$_SESSION['password'],'MemoServ','send',Array($_GET['to'],$_GET['message']));
+			if($res[0]){
+				if(substr($res[1],-19) == ' is not registered.'){
+					die('{"code":1,"message":"User '.$_GET['to'].' does not exist"}');
+				}else{
+					die('{"code":0,"message":"Memo Sent"}');
+				}
+			}else{
+				die('{"code":1,"message":"Cannot send memo: '+$res[1]+'"}');
+			}
+		break;
+		case 'delete-memo':
+			$u or die('{"code":1,"message":"You have been logged out"}');
+			isset($_GET['id']) or die('{"code":1,"message":"No id given"}');
+			$res = atheme_command(get_conf('xmlrpc-server'),get_conf('xmlrpc-port'),get_conf('xmlrpc-path'),USER_IP,$_COOKIE['user'],$_SESSION['password'],'MemoServ','delete',Array($_GET['id']));
+			if(!$res[0]){
+				die('{"code":1,"message":"Cannot send memo: '+$res[1]+'"}');
+			}
+			die('{"code":0}');
+		break;
+		case 'persona-login':
+			if($u){
+				$register = true;
+			}else{
+				$register = false;
+			}
+			$url = get_conf('persona-endpoint');
+			$assert = filter_input(
+				INPUT_POST,
+				'assertion',
+				FILTER_UNSAFE_RAW,
+				FILTER_FLAG_STRIP_LOW|FILTER_FLAG_STRIP_HIGH
+			);
+			$params = 'assertion='.urlencode($assert).'&audience='.urlencode(get_conf('persona-audience'));
+			$ch = curl_init();
+				$options = array(
+				CURLOPT_URL => $url,
+				CURLOPT_RETURNTRANSFER => TRUE,
+				CURLOPT_POST => 2,
+				CURLOPT_SSL_VERIFYPEER => 0,
+				CURLOPT_SSL_VERIFYHOST => 2,
+				CURLOPT_POSTFIELDS => $params
+			);
+			curl_setopt_array($ch, $options);
+			$result = curl_exec($ch);
+			curl_close($ch);
+			$result = json_decode($result);
+			if($result->status == 'okay'){
+				if($register && !add_email($u['id'],$result->email)){
+					die('{"code":1,"message":"Failed to add email '.$result->email.' to user '.$u['nick'].'"}');
+				}elseif(!$register && !$u = get_user_for_email($result->email)){
+					die('{"code":1,"message":"Email does not match any users"}');
+				}
+				setcookie('personaUser',$result->email,null,'/');
+				$pass = null;
+				if(isset($_SESSION['password']) && !is_null($_SESSION['password']) && $_SESSION['password'] != ''){
+					$pass = $_SESSION['password'];
+				}
+				$types = get_user_types($u['id']);
+				$r = login($u['nick'],$pass,'persona',$types[0]);
+				if($r !== true){
+					if($r){
+						die('{"code":2,"message":"'.$r.'"}');
+					}else{
+						die('{"code":2}');
+					}
+				}else{
+					die('{"code":0,"assertion":'.json_encode($result).'}');
+				}
+			}else{
+				die('{"code":1,"message":"'.$result->reason.'"}');
+			}
+		break;
+		case 'persona-remove':
+			$u or die('{"code":1,"message":"You have been logged out"}');
+			isset($_GET['id']) or die('{"code":1,"message":"No ID set"}');
+			if(!remove_email($u['id'],$_GET['id'],true)){
+				die('{"code":1,"message":"Could not remove email address"}');
+			}
+			die('{"code":0}');
+		break;
+		case '2-factor-register':
+			$r = register_token();
+			if($r !== true){
+				die('{"code":1,"message":"'.$r.'"}');
+			}
+			die('{"code":0}');
+		break;
+		case '2-factor-delete':
+			$u or die('{"code":1,"message":"You have been logged out"}');
+			$r = delete_token($u['id']);
+			if($r !== true){
+				die('{"code":1,"message":"'.$r.'"}');
+			}
+			die('{"code":0,"message":"2-factor disabled."}');
+		break;
+		case 'ping':
+			$u or die('{"code":1,"message":"You have been logged out"}');
+			die('{"code":0}');
+		break;
+		case 'newpass':
+			$u && isset($_GET['password']) && isset($_GET['newpass']) or die('{"code":2,"message":"Make sure that everything is filled in. Try reloading if it is."}');
+			$u['password'] == mkpasswd($_GET['password'],$u['salt']) or die('{"code":2,"message":"Invalid password"}');
+			$u['api_key'] == $_COOKIE['key'] or die('{"code":3,"message":"Not Logged in to use '.$u['nick'].' with key '.$u['api_key'].' != '.$_COOKIE['key'].'."}');
+			if($_COOKIE['type'] == 'user'){
+				$res = atheme_command(get_conf('xmlrpc-server'),get_conf('xmlrpc-port'),get_conf('xmlrpc-path'),USER_IP,$u['nick'],$_GET['password'],'NickServ','set',Array('password',trim($_GET['newpass'])));
+				if($res[0] === false){
+					die('{"code":2,"message":"Could not update password with nickserv: '.$res[1].'"}');
+				}else{
+					$_SESSION['password'] = $_GET['newpass'];
+				}
+			}
+			query("UPDATE users u SET u.password='%s' WHERE u.id=%d",Array(mkpasswd($_GET['newpass']),$u['id']));
+			die('{"code":0}');
+		break;
+		case 'sync-pass':
+			$u && isset($_SESSION['password'])or die('{"code":2,"message":"Make sure that everything is filled in. Try reloading if it is."}');
+			$u['api_key'] == $_COOKIE['key'] or die('{"code":3,"message":"Not Logged in to use '.$u['nick'].' with key '.$u['api_key'].' != '.$_COOKIE['key'].'."}');
+			$_COOKIE['type'] == 'user' or die('{"code":3,"message":"Must be logged in with type user to sync pass"}');
+			$res = atheme_login(get_conf('xmlrpc-server'),get_conf('xmlrpc-port'),get_conf('xmlrpc-path'),$u['nick'],$_SESSION['password']);
+			if($res[0] === false){
+				die('{"code":2,"message":"Could not verify with nickserv: '.$res[1].'"}');
+			}
+			query("UPDATE users u SET u.password='%s' WHERE u.id=%d",Array(mkpasswd($_SESSION['password']),$u['id']));
+			die('{"code":0,"message":"Nickserv password synchronized with main account"}');
+		break;
+		case 'role':
+			$u && isset($_GET['type']) or die('{"code":2,"message":"Make sure that everything is filled in. Try reloading if it is."}');
+			setcookie('type',$_GET['type'],null,'/');
+			die('{"code":0}');
+		break;
+		case 'user':
+			$u or die('{"code":10,"message":"Not logged in"}');
+			isset($_GET['id']) or die('{"code":2,"message":"No user set."}');
+			isset($_GET['email']) or die('{"code":2,"message":"No email set."}');
+			isset($_GET['real_name']) or die('{"code":2,"message":"No real name set."}');
+			isset($_GET['nick']) or die('{"code":2,"message":"No nick set."}');
+			$user = get_user_from_id_obj($_GET['id']) or die('{"code":2,"message":"User with id '.$_GET['id'].' does not exist. You should reload the page."}');
+			if($u['id'] == $user['id']){
+				setcookie('user',$_GET['nick'],null,'/');
+			}
+			query("UPDATE users u SET u.nick='%s', u.real_name='%s', u.email='%s' WHERE u.id=%d",Array($_GET['nick'],$_GET['real_name'],$_GET['email'],$_GET['id'])) or die('{"code":2,"message":"Unable to update user"}');
+			die(ircrehash());
+		break;
+		case 'oper':
+			$u or die('{"code":10,"message":"Not logged in"}');
+			isset($_GET['id']) or die('{"code":2,"message":"No user set."}');
+			isset($_GET['nick']) or die('{"code":2,"message":"No nick set."}');
+			isset($_GET['swhois']) or die('{"code":2,"message":"No profile set."}');
+			$oper = get_oper_from_id_obj($_GET['id']) or die('{"code":2,"message":"Oper with id '.$_GET['id'].' does not exist. You should reload the page."}');
+			if(isset($_GET['password']) && $_GET['password'] != ""){
+				query("UPDATE opers o SET o.nick='%s', o.swhois='%s', o.password='%s', o.password_type_id=2 WHERE o.id=%d",Array($_GET['nick'],$_GET['swhois'],mkpasswd($_GET['password']),$_GET['id'])) or die('{"code":2,"message":"Unable to update oper"}');
+			}else{
+				query("UPDATE opers o SET o.nick='%s', o.swhois='%s' WHERE o.id=%d",Array($_GET['nick'],$_GET['swhois'],$_GET['id'])) or die('{"code":2,"message":"Unable to update oper"}');
+			}
+			die(ircrehash());
+		break;
+		case 'config':
+			foreach($_GET as $key => $val){
+				set_conf($key,$val,get_conf_type($key)) or die('{"code":1,"message":"Failed to update setting: '.$key.' with value: '.$val.'"}');
+			}
+			die('{"code":0}');
+		break;
+		case 'rehash':
+			$u or die('{"code":10,"message":"Not logged in"}');
+			die(ircrehash());
+		break;
+		default:
+			die('{"code":1,"message":"Invalid Action '.$_GET['action'].'"}');
+	}
+?>

BIN
site/img/collapse.png


BIN
site/img/expand.png


BIN
site/img/file.png


BIN
site/img/folder.png


BIN
site/img/persona-logo.png


BIN
site/img/persona.png


+ 95 - 0
site/index.css

@@ -0,0 +1,95 @@
+html,body{
+	margin: 0;
+	padding: 0;
+	width: 100%;
+	height: 100%;
+	overflow: hidden;
+	background-color: #726A8B;
+	background-image: url('//www.omnimaga.org/Themes/omnimaga_retro/images/bg.png');
+	background-repeat: repeat-x;
+	background-attachment: fixed;
+	background-position: center top;
+	background-clip: border-box;
+	background-origin: padding-box;
+	background-size: auto auto;
+	color: white;
+}
+.ui-state-default.ui-widget .link{
+	text-decoration: underline;
+	cursor: pointer;
+	color: blue;
+}
+.countries-autocomplete{
+	color: black;
+}
+.ui-dialog.ui-front{
+	z-index: 101;
+}
+.ui-widget-content{
+	background-color: rgba(0,0,0,0.5) !important;
+}
+.transparent{
+	background-color: transparent !important;
+	background-image: none !important;
+}
+.left{
+	float: left;
+	clear: none;
+}
+.right{
+	float: right;
+	clear: none;
+}
+.clearR{
+	clear: right;
+}
+.clearL{
+	clear: left;
+}
+.fill{
+	width: 100%;
+	display: inline-block;
+}
+.server-prop{
+	font-weight: bold;
+	font-style: italic;
+	cursor: pointer;
+	border-style: solid;
+	border-color: black;
+	border-width: 1px;
+	border-radius: 3px;
+}
+h1,h2,h3,h4,ul,li{
+	margin: 0;
+}
+form,.table{
+	display: table;
+}
+form label, form label + span,.cell{
+	display: table-cell;
+	padding: 3px;
+}
+form label{
+	font-weight: bold;
+}
+form label + span>*{
+	width: 100%;
+}
+.row{
+	display: table-row;
+}
+.ui-tabs.ui-widget-content.ui-widget{
+	overflow: auto;
+}
+.tree{
+	border-collapse: collapse;
+}
+.tree tr td{
+	color: #777;
+}
+.tree tr:not(.treegrid-collapsed) td{
+	border-color: #777;
+	border-bottom-style: solid;
+	border-bottom-width: 1px;
+	padding-right: 3px;
+}

+ 489 - 0
site/index.js

@@ -0,0 +1,489 @@
+$(function(){
+	if(location.host != purl(__HOSTNAME__).attr('host')){
+		location.href = __HOSTNAME__;
+	}
+	var dialogs = $('#dialogs').children('div'),
+		memos,
+		news,
+		templates = [],
+		logout = function(){
+			$.removeCookie('user',{
+				path: '/'
+			});
+			$.removeCookie('key',{
+				path: '/'
+			});
+			$.removeCookie('token',{
+				path: '/'
+			});
+			$.removeCookie('PHPSESSID',{
+				path: '/'
+			});
+			$.ajax(__HOSTNAME__+'site/api/',{
+				data: {
+					action: 'logout'
+				},
+				complete: function(){
+					location.reload();
+				},
+				dataType: 'json'
+			});
+		};
+	$('script[id^=template-]').each(function(){
+		templates[this.id.substr(9)] = Handlebars.compile($(this).html());
+	});
+	Handlebars.registerHelper('html',function(body){
+		return new Handlebars.SafeString(body.replace(/(\b(https?|ftps?|file|irc):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig,"<a class='link' href='$1'>$1</a>"));
+	});
+	$('form').submit(function(){
+		var form = $(this),
+			btn = form.children('input[type=submit]'),
+			action = form.children('input[type=hidden][name=action]').val();
+		$.ajax(__HOSTNAME__+'site/api/?'+form.serialize(),{
+			success: function(d){
+				if(d.log){
+					console.log(d.log);
+				}
+				btn.removeAttr('disabled');
+				if(d.code === 0){
+					switch(action){
+						case 'oper':
+							form.find('input[name=password]').val('');
+							alert('Oper updated');
+						break;
+						default:
+							location.reload();
+					}
+				}else{
+					alert(d.message);
+				}
+			},
+			error: function(xhr,msg,e){
+				console.error(e);
+				alert("Could not submit the form: "+msg);
+				btn.removeAttr('disabled');
+			},
+			dataType: 'json'
+		});
+		btn.attr('disabled','disabled');
+		return false;
+	}).children('input[type=hidden][name=action]').removeAttr('disabled');
+	$('#logout').click(logout);
+	$('#newpass-button').click(function(){
+		$('#newpass-diag').dialog('open');
+	});
+	$('#roles-button').click(function(){
+		$('#roles-diag').dialog('open');
+	});
+	$('#rehash-servers').click(function(){
+		$.ajax(__HOSTNAME__+'site/api/?action=rehash',{
+			success: function(d){
+				if(d.log){
+					console.log(d.log);
+				}
+				alert(d.message);
+				$('#rehash-servers').removeAttr('disabled');
+			},
+			error: function(xhr,msg,e){
+				console.error(e);
+				alert("Could not rehash the servers: "+msg);
+				$('#rehash-servers').removeAttr('disabled');
+			},
+			dataType: 'json'
+		});
+		$(this).attr('disabled','disabled');
+		return false;
+	});
+	$('#2-factor-disable').click(function(){
+		var btn = $(this);
+		$.ajax(__HOSTNAME__+'site/api/?action=2-factor-delete',{
+			success: function(d){
+				if(d.log){
+					console.log(d.log);
+				}
+				alert(d.message);
+				btn.removeAttr('disabled');
+				location.reload();
+			},
+			error: function(xhr,msg,e){
+				console.error(e);
+				alert("Could not disable 2-factor: "+msg);
+				btn.removeAttr('disabled');
+			},
+			dataType: 'json'
+		});
+		$(this).attr('disabled','disabled');
+		return false;
+	});
+	$('#sync-pass').click(function(){
+		var btn = $(this);
+		$.ajax(__HOSTNAME__+'site/api/?action=sync-pass',{
+			success: function(d){
+				if(d.log){
+					console.log(d.log);
+				}
+				alert(d.message);
+				btn.removeAttr('disabled');
+			},
+			error: function(xhr,msg,e){
+				console.error(e);
+				alert("Could not synchronize your password: "+msg);
+				btn.removeAttr('disabled');
+			},
+			dataType: 'json'
+		});
+		btn.attr('disabled','disabled');
+		return false;
+	});
+	$('#persona-register').hover(function(){
+		$(this).addClass('ui-state-hover');
+	},function(){
+		$(this).removeClass('ui-state-hover');
+	}).click(function(){
+		if(confirm("This is an admin only feature. Continue?")){
+			navigator.id.request({
+				siteName: 'Omninet'
+			});
+		}
+	});
+	if(navigator.id){
+		navigator.id.watch({
+			loggedInUser: $.cookie('personaUser'),
+			onlogin: function(assertion){
+				$.ajax({
+					type: 'post',
+					url: __HOSTNAME__+'site/api/?action=persona-login',
+					data: {
+						assertion: assertion
+					},
+					success: function(d){
+						if(d.code !== 0){
+							if(d.message){
+								console.log(d.message);
+								alert(d.message);
+							}
+						}
+						location.reload();
+					},
+					error: function(xhr,s,e){
+						navigator.id.logout();
+						alert("Login failure: " + e);
+					}
+				});
+			},
+			onlogout: function(){
+				//$('#logout').click();
+			}
+		});
+	}
+	$('button[id^=persona-remove-]').each(function(){
+		var id = this.id.substr(15),
+			btn = $(this);
+		btn.click(function(){
+			$.ajax(__HOSTNAME__+'site/api/?action=persona-remove&id='+id,{
+				success: function(d){
+					if(d.log){
+						console.log(d.log);
+					}
+					if(d.message){
+						alert(d.message);
+					}
+					location.reload();
+				},
+				error: function(xhr,msg,e){
+					console.error(e);
+					alert("Could not remove persona address: "+msg);
+					btn.removeAttr('disabled');
+				},
+				dataType: 'json'
+			});
+			btn.attr('disabled','disabled');
+			return false;
+		});
+	});
+	$('.server-opers,.server-owner,.server-children,.server-parent').click(function(){
+		$(this).next().toggle();
+	}).next().hide();
+	$('.button,button,input[type=button],input[type=submit]').button();
+	$('.tabs').tabs({
+		activate: function(e,ui){
+			var url = $.url(),
+				params = url.data.param.query;
+			params.tab = ui.newPanel.attr('id');
+			History.pushState({},document.title,url.attr('path')+'?'+$.param(params)+url.attr('anchor'));
+		},
+		create: function(e,ui){
+			$(window).trigger('statechange');
+		},
+		heightStyle: 'fill'
+	}).addClass('transparent').each(function(){
+		var tabs = $(this);
+		tabs.parent().resize(function(){
+			tabs.tabs('refresh');
+		});
+	});
+	dialogs.dialog({
+		modal: true,
+		draggable: false,
+		autoOpen: false,
+		width: 500
+	});
+	$('.menu').menu();
+	$(window).on('statechange',function(){
+		var url = $.url(),
+			tab = url.param('tab'),
+			params = url.data.param.query,
+			tabel = $('.tabs').children('ul').children('li').children('a[href="#'+tab+'"]');
+		if(tab && tabel.length == 1){
+			$('.tabs').tabs('option','active',tabel.parent().index());
+		}else{
+			var href = $('.tabs').children('ul').children('li').children('a');
+			if(href.length > 0){
+				href = href.get(0).href;
+			}else{
+				href = '';
+			}
+			params.tab = $.url(href).attr('fragment');
+			History.pushState({},document.title,url.attr('path')+'?'+$.param(params)+url.attr('anchor'));
+		}
+	}).trigger('statechange').resize(function(){
+		dialogs.each(function(){
+			var d = $(this);
+			if(d.dialog('isOpen')){
+				d.dialog("option", "position", "center");
+			}
+		});
+		var b = $('#user-menu-button');
+		if(b.length > 0){
+			$('#user-menu').offset({
+				top: b.offset().top
+			});
+		}
+	});
+	$('#login-diag,#verify-diag').dialog('option',{
+		closeOnEscape: false,
+		close: function(){
+			location.href = 'http://omnimaga.org';
+		},
+		position:{
+			my: "center",
+			at: "center",
+			of: window
+		}
+	}).dialog('open');
+	$('#verify-diag').dialog('option','close',logout);
+	$('.accordion').accordion({
+		collapsible: true,
+		active: false,
+		heightStyle: 'content'
+	}).css('max-height','500px');
+	$('.tree').treegrid({
+		initialState: 'collapsed'
+	});
+	$('#user-menu-button').click(function(){
+		$('#user-menu').show();
+	});
+	$('#user-menu').css({
+		position: 'fixed',
+		right: '0'
+	}).hover(function(){},function(){
+		$(this).hide();
+	}).click(function(){
+		$(this).hide();
+	}).hide();
+	if(!Modernizr.inputtypes.date){
+		$('input[type=date]').datepicker({
+			dateFormat: 'yy-mm-dd'
+		});
+	}
+	if(!Modernizr.inputtypes.datetime){
+		$('input[type=datetime]').datetimepicker({
+			dateFormat: 'yy-mm-dd',
+			timeFormat:'HH:mm:ssZ'
+		});
+	}
+	if(!Modernizr.inputtypes.number){
+		$('input[type=number]').spinner();
+	}
+	$('body').show();
+	window.ServerPing = function(){
+		console.log("Server Ping");
+		$.ajax(__HOSTNAME__+'site/api/?action=ping',{
+			success: function(d){
+				if(d.log){
+					console.log(d.log);
+				}
+				if(d.message){
+					alert(d.message);
+				}
+				if(d.code!==0){
+					location.reload();
+				}
+			},
+			error: function(xhr,msg,e){
+				console.error(e);
+				alert("Could not ping server: "+msg);
+				location.reload();
+			},
+			dataType: 'json'
+		});
+		setTimeout(window.ServerPing,1000*60*5); // Every 5 minutes
+	};
+	window.FetchMemos = function(once){
+		console.log("Fetching Memos");
+		$.ajax(__HOSTNAME__+'site/api/?action=get-memos',{
+			success: function(d){
+				if(d.log){
+					console.log(d.log);
+				}
+				if(d.message){
+					alert(d.message);
+				}
+				if(d.code!==0){
+					location.reload();
+				}
+				var i,
+					m;
+				if(d.memos){
+					for(i in d.memos){
+						m = d.memos[i];
+						m.date = m.date.year+'-'+m.date.month+'-'+m.date.day+' '+m.date.time;
+						d.memos[i] = m;
+					}
+					if(typeof memos != 'undefined' && !once && ($(d.memos).not(memos).length !== 0 || $(memos).not(d.memos).length !== 0)){
+						alert('New memo');
+					}
+					memos = d.memos;
+				}
+				$('#memos').html(templates.memos(d)).find('button').button();
+			},
+			error: function(xhr,msg,e){
+				console.error(e);
+				alert("Could not contact server: "+msg);
+				location.reload();
+			},
+			dataType: 'json'
+		});
+		if(!once){
+			setTimeout(window.ServerPing,1000*60); // Every minute
+		}
+	};
+	window.DeleteMemoFromButton = function(){
+		window.DeleteMemo($(this).parent());
+	};
+	window.ReplyToMemoFromButton = function(){
+		window.ReplyToMemo($(this).parent());
+	};
+	window.ReplyToMemo = function(div){
+		var from = div.find('.memo-from').text().trim();
+		$('#memo-diag').dialog('open').find('input[name=to]').val(from);
+		$('#memo-diag').find('input[name=message]').select();
+	};
+	window.DeleteMemos = function(){
+		window.DeleteMemo($('<div>').attr('id','memo-all'),function(){
+			window.FetchMemos(true);
+		});
+	};
+	window.DeleteMemo = function(div,callback){
+		var id = $(div).attr('id').substr(5);
+		console.log("Deleting memo: "+id);
+		$.ajax(__HOSTNAME__+'site/api/?action=delete-memo&id='+id,{
+			success: function(d){
+				if(d.log){
+					console.log(d.log);
+				}
+				if(d.message){
+					alert(d.message);
+				}
+				if(d.code!==0){
+					location.reload();
+				}
+				div.remove();
+				if(typeof callback != 'undefined'){
+					callback();
+				}
+			},
+			error: function(xhr,msg,e){
+				console.error(e);
+				alert("Could not ping server: "+msg);
+				location.reload();
+			},
+			dataType: 'json'
+		});
+	};
+	window.FetchNews = function(once){
+		console.log("Fetching News");
+		$.ajax(__HOSTNAME__+'site/api/?action=get-news',{
+			success: function(d){
+				if(d.log){
+					console.log(d.log);
+				}
+				if(d.message){
+					alert(d.message);
+				}
+				if(d.code!==0){
+					location.reload();
+				}
+				var i,
+					n;
+				if(d.news){
+					d.news = d.news.reverse();
+					for(i in d.news){
+						n = d.news[i];
+						n.date = n.date.year+'-'+n.date.month+'-'+n.date.day+' '+n.date.time;
+						d.news[i] = n;
+					}
+					if(typeof news != 'undefined' && !once && ($(d.news).not(news).length !== 0 || $(news).not(d.news).length !== 0)){
+						alert('New news item');
+					}
+					news = d.news;
+				}
+				$('#news').html(templates.news(d)).find('button').button();
+			},
+			error: function(xhr,msg,e){
+				console.error(e);
+				alert("Could not contact server: "+msg);
+				location.reload();
+			},
+			dataType: 'json'
+		});
+		if(!once){
+			setTimeout(window.ServerPing,1000*60); // Every minute
+		}
+	};
+	window.FetchChannels = function(once){
+		console.log("Fetching Channels");
+		$.ajax(__HOSTNAME__+'site/api/?action=get-channels',{
+			success: function(d){
+				if(d.log){
+					console.log(d.log);
+				}
+				if(d.message){
+					alert(d.message);
+				}
+				if(d.code!==0){
+					location.reload();
+				}
+				var i,
+					n;
+				if(d.channels){
+					for(i in d.channels){
+						n = d.channels[i];
+						d.channels[i] = n;
+					}
+				}
+				$('#channels').html(templates.channels(d)).find('button').button();
+			},
+			error: function(xhr,msg,e){
+				console.error(e);
+				alert("Could not contact server: "+msg);
+				location.reload();
+			},
+			dataType: 'json'
+		});
+		if(!once){
+			setTimeout(window.ServerPing,1000*60); // Every minute
+		}
+	};
+	$('body').resize();
+});

+ 470 - 0
site/index.php

@@ -0,0 +1,470 @@
+<?php
+	require_once(dirname(dirname(__FILE__)).'/header.php');
+	header('X-UA-Compatible: IE=Edge');
+	global $user;
+	if($user = is_logged_in()){
+		if(has_flag($user,'a')){
+			$servers = get_servers_obj();
+			$opers = get_opers_obj();
+		}else{
+			$servers = get_servers_for_current_user_obj();
+			$opers = get_opers_for_current_user_obj();
+		}
+	}
+	$dialogs = array();
+?>
+<html>
+	<head>
+		<meta charset="UTF-8">
+		<title>Omninet</title>
+		<script>
+			__HOSTNAME__ = '<?php echo HOSTNAME; ?>';
+		</script>
+		<link href="<?php echo HOSTNAME; ?>site/favicon.ico" rel="icon" type="image/x-icon" />
+		<script src="<?php echo HOSTNAME; ?>site/Modernizr.js"></script>
+		<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
+		<script src="//cdnjs.cloudflare.com/ajax/libs/jqueryui/1.10.3/jquery-ui.min.js"></script>
+		<script src="//cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.0/jquery.cookie.min.js"></script>
+		<script src="//cdnjs.cloudflare.com/ajax/libs/selectize.js/0.8.5/js/selectize.min.js"></script>
+		<script src="<?php echo HOSTNAME; ?>site/jquery.treegrid.js"></script>
+		<script src="//cdnjs.cloudflare.com/ajax/libs/history.js/1.8/bundled/html5/jquery.history.min.js"></script>
+		<script src="<?php echo HOSTNAME; ?>site/jquery.timepicker.js"></script>
+		<script src="<?php echo HOSTNAME; ?>site/jquery.ba-resize.min.js"></script>
+		<?php if(get_conf('2-factor-method') == 'authy'){ ?>
+			<script src="//cdnjs.cloudflare.com/ajax/libs/authy-forms.js/2.0/form.authy.min.js"></script>
+		<?php } ?>
+		<script src="//cdnjs.cloudflare.com/ajax/libs/jquery-url-parser/2.3.1/purl.min.js"></script>
+		<script src="//cdnjs.cloudflare.com/ajax/libs/handlebars.js/1.3.0/handlebars.min.js"></script>
+		<?php if(get_conf('persona-endpoint') != 'none'){ ?>
+			<script src="//login.persona.org/include.js"></script>
+		<?php } ?>
+		<script src="<?php echo HOSTNAME; ?>site/index.js"></script>
+		<link href="//code.jquery.com/ui/1.10.4/themes/dot-luv/jquery-ui.css" rel="stylesheet"/>
+		<link href="//cdnjs.cloudflare.com/ajax/libs/selectize.js/0.8.5/css/selectize.css" rel="stylesheet"/>
+		<link href="//cdnjs.cloudflare.com/ajax/libs/selectize.js/0.8.5/css/selectize.default.css" rel="stylesheet"/>
+		<link href="<?php echo HOSTNAME; ?>site/jquery.treegrid.css" rel="stylesheet"/>
+		<link href="<?php echo HOSTNAME; ?>site/jquery.timepicker.css" rel="stylesheet"/>
+		<?php if(get_conf('2-factor-method') == 'authy'){ ?>
+			<link href="//cdnjs.cloudflare.com/ajax/libs/authy-forms.css/2.0/form.authy.min.css" rel="stylesheet"/>
+		<?php } ?>
+		<link href="<?php echo HOSTNAME; ?>site/index.css" rel="stylesheet"/>
+		<?php
+			if($user){
+				echo "<script>$(function(){";
+				if(is_logged_in() && is_verified()){
+					echo "window.ServerPing();";
+				}
+				if(has_flag($user,'u')){
+					echo "window.FetchMemos();";
+					echo "window.FetchNews();";
+					echo "window.FetchChannels();";
+				}
+				echo "});</script>";
+				if(has_flag($user,'u')){ ?>
+					<script id="template-memos" type="text/x-handlebars-template">
+						<button class="button" value="Refresh" onclick="window.FetchMemos(true);">
+							Refresh
+						</button>
+						<button style="background-color:green;background-image:none;" class="button" value="New Memo" id="send-memo" onclick="$('#memo-diag').dialog('open');">
+							New Memo
+						</button>
+						<button class="button" style="background-color:red;background-image:none;" value="Delete All" onclick="return window.DeleteMemos();">
+							Delete All
+						</button>
+						{{#each memos}}
+							<div style="padding: 5px;" class="ui-widget ui-state-default ui-corner-all" id="memo-{{this.id}}">
+								<div>
+									<span>
+										From:
+									</span>
+									<span style="font-weight:normal;" class='memo-from'>
+										{{this.from}}
+									</span>
+								</div>
+								<div>
+									<span>
+										Sent:
+									</span>
+									<span style="font-weight:normal;" class='memo-date'>
+										{{this.date}}
+									</span>
+								</div>
+								<div>
+									<span>
+										Body:
+									</span>
+									<span style="font-weight:normal;" class="memo-body">
+										{{html this.body}}
+									</span>
+								</div>
+								<button class="button" value="Reply" onclick="return window.ReplyToMemoFromButton.call(this);">
+									Reply
+								</button>
+								<button style="background-color:red;background-image:none;" class="button" value="Delete" onclick="return window.DeleteMemoFromButton.call(this);">
+									Delete
+								</button>
+							</div>
+						{{/each}}
+					</script>
+					<script id="template-news" type="text/x-handlebars-template">
+						<button value="Refresh" onclick="window.FetchNews(true);">
+							Refresh
+						</button>
+						{{#each news}}
+							<div id="news-{{this.id}}" class="ui-widget ui-state-default ui-corner-all" style="padding:5px;">
+								<h2>
+									{{this.title}}
+								</h2>
+								<div>
+									<span>
+										From:
+									</span>
+									<span style="font-weight:normal;">
+										{{this.from}}
+									</span>
+								</div>
+								<div>
+									<span>
+										Sent:
+									</span>
+									<span style="font-weight:normal;">
+										{{this.date}}
+									</span>
+								</div>
+								<p style="font-weight:normal;">
+									{{html this.body}}
+								</p>
+							</div>
+						{{/each}}
+					</script>
+					<script id="template-channels" type="text/x-handlebars-template">
+						<button value="Refresh" onclick="window.FetchChannels(true);">
+							Refresh
+						</button>
+						<button value="New Channel" style="background-color:green;background-image:none;" onclick="">
+							New Channel
+						</button>
+						{{#each channels}}
+							<div id="channel-{{this.name}}" class="ui-widget ui-state-default ui-corner-all" style="padding:5px;">
+								{{this.name}}
+								<br/>
+								Flags:
+								<ul>
+									{{#each this.flags}}
+										<li>{{this.name}}</li>
+									{{/each}}
+								</ul>
+								<button value="Delete" style="background-color:red;background-image:none;" onclick="">
+									Delete
+								</button>
+							</div>
+						{{/each}}
+					</script>
+				<?php }
+			}
+		?>
+	</head>
+	<body style="display:none;">
+		<?php
+			$flag = is_verified();
+			if($user && $flag){
+		?>
+			<div class="tabs">
+				<ul>
+					<?php
+						if(has_flag($user,'u')){ ?>
+							<li><a href="#news">News</a></li>
+							<li><a href="#memos">Memos</a></li>
+							<li><a href="#channels">Channels</a></li>
+						<?php }
+						echo has_flag($user,'n')?'<li><a href="#servers">Servers</a></li>':'';
+						echo has_flag($user,'o')?'<li><a href="#opers">Opers</a></li>':'';
+						echo has_flag($user,'a')?'<li><a href="#config">Configuration</a></li>':'';
+					?>
+					<li><a href="#profile">Profile</a></li>
+					<div id="user-menu-button" class="right button">
+						<?php echo $user['nick']; ?>
+					</div>
+				</ul>
+				<?php if(has_flag($user,'n')){?>
+					<div id="servers">
+						<?php
+							echo get_servers_list_html($servers);
+						?>
+					</div>
+				<?php }
+				if(has_flag($user,'o')){?>
+					<div id="opers">
+						<?php echo get_opers_html($opers); ?>
+					</div>
+				<?php }
+				if(has_flag($user,'a')){ ?>
+					<div id="config">
+						<?php echo render_configuration_table(); ?>
+					</div>
+				<?php }
+				if(has_flag($user,'u')){ ?>
+					<div id="news"></div>
+					<div id="memos"></div>
+					<div id="channels"></div>
+				<?php 
+					array_push($dialogs,array(
+						'id'=>'memo-diag',
+						'type'=>'form',
+						'form_id'=>'memo',
+						'form_submit_label'=>'Send',
+						'form_fields'=>array(
+							array(
+								'name'=>'to',
+								'label'=>'To',
+								'type'=>'string',
+								'value'=>''
+							),
+							array(
+								'name'=>'message',
+								'label'=>'Message',
+								'type'=>'string',
+								'value'=>''
+							),
+							array(
+								'name'=>'action',
+								'type'=>'hidden',
+								'value'=>'send-memo'
+							)
+						)
+					));
+				} ?>
+				<div id="profile">
+					<?php
+						echo get_user_html($user);
+						if(has_flag($user,'a') || has_flag($user,'o') || has_flag($user,'n')){
+							if(!isset($user['secret_key']) || is_null($user['secret_key']) || $user['secret_key'] == ''){
+								switch(get_conf('2-factor-method')){
+									case 'authy':
+										echo '<div class="login-form">Enable 2-factor Authentication'.get_form_html('2-factor',array(
+											array(
+												'name'=>'country-code',
+												'label'=>'Country',
+												'type'=>'text',
+												'attributes'=>array(
+													'id'=>'authy-countries'
+												)
+											),
+											array(
+												'name'=>'cellphone',
+												'label'=>'Cell #',
+												'type'=>'text',
+												'attributes'=>array(
+													'id'=>'authy-cellphone'
+												)
+											),
+											array(
+												'name'=>'action',
+												'type'=>'hidden',
+												'value'=>'2-factor-register'
+											)
+										),'Submit').'</div>';
+									break;
+									case 'google-authenticator':
+										$api = get_api();
+										$_SESSION['secret_key'] = $api->createSecret();
+										echo '<div class="login-form">Enable 2-factor Authentication'.get_form_html('2-factor',array(
+											array(
+												'type'=>'custom',
+												'html'=>"<img src='data:image/png;base64,".base64_encode(file_get_contents($api->getQRCodeGoogleUrl('Omninet',$_SESSION['secret_key'])))."'/>"
+											),
+											array(
+												'name'=>'token',
+												'label'=>'Token',
+												'type'=>'text'
+											),
+											array(
+												'name'=>'action',
+												'type'=>'hidden',
+												'value'=>'2-factor-register'
+											)
+										),'Submit').'</div>';
+									break;
+									default:
+								}
+							}else{
+								switch(get_conf('2-factor-method')){
+									case 'authy':case 'google-authenticator':
+										echo "<button id='2-factor-disable' value='Disable 2-factor'>Disable 2-factor</button>";
+									break;
+									default:
+								}
+							}
+							if(get_conf('persona-endpoint') != 'none'){
+								echo "<div><span id='persona-register' class='ui-button ui-widget ui-state-default ui-corner-all' style='overflow:hidden;height:42px;padding:0px 20px 0px 0px;vertical-align:middle;'><img style='height:100%;float:left;' src='img/persona-logo.png'/><span style='display:inline-block;line-height:42px;'>Link Persona</span></span></div>";
+								$emails = get_emails($user['id'],true);
+								foreach($emails as $k => $email){
+									echo "<div><button id='persona-remove-{$email['id']}' value='Remove'>Remove</button>{$email['email']}</div>";
+								}
+							}
+						}
+					?>
+				</div>
+			</div>
+			<ul class="menu" id="user-menu">
+				<li><a id="roles-button">Switch Role</a></li>
+				<?php if(has_flag($user,'n')||has_flag($user,'a')){?>
+					<li><a id="rehash-servers">Rehash</a></li>
+				<?php } ?>
+				<li><a id="newpass-button">Change Password</a></li>
+				<?php if(has_flag($user,'u')){ ?>
+					<li><a id="sync-pass">Sync Pass</a></li>
+				<?php } ?>
+				<li><a id="logout">Logout</a></li>
+			</ul>
+			<?php
+					array_push($dialogs,array(
+						'id'=>'newpass-diag',
+						'type'=>'form',
+						'form_id'=>'newpass',
+						'form_submit_label'=>'Change Password',
+						'form_fields'=>array(
+							array(
+								'name'=>'password',
+								'label'=>'Password',
+								'type'=>'password',
+								'value'=>''
+							),
+							array(
+								'name'=>'newpass',
+								'label'=>'New Password',
+								'type'=>'password',
+								'value'=>''
+							),
+							array(
+								'name'=>'action',
+								'type'=>'hidden',
+								'value'=>'newpass'
+							)
+						)
+					));
+					$roles = array(array(
+						'value'=>'user',
+						'label'=>'User'
+					));
+					if($res = query("SELECT rt.name AS value,rt.description AS label FROM user_role_types rt JOIN user_roles r ON r.user_role_id = rt.id JOIN users u ON r.user_id = u.id WHERE u.id = %d",array($user['id']))){
+						while($role = $res->fetch_assoc()){
+							array_push($roles,$role);
+						}
+					}
+					array_push($dialogs,array(
+						'id'=>'roles-diag',
+						'type'=>'form',
+						'form_id'=>'roles',
+						'form_submit_label'=>'Switch',
+						'form_fields'=>array(
+							array(
+								'name'=>'type',
+								'label'=>'Type',
+								'type'=>'select',
+								'values'=>$roles,
+								'value'=>isset($_COOKIE['type'])?$_COOKIE['type']:'user'
+							),
+							array(
+								'name'=>'action',
+								'type'=>'hidden',
+								'value'=>'role'
+							)
+						)
+					));
+				}elseif($user && !$flag){
+					array_push($dialogs,array(
+						'id'=>'verify-diag',
+						'type'=>'form',
+						'autocomplete'=>'off',
+						'form_id'=>'verify',
+						'form_submit_label'=>'Login',
+						'form_fields'=>array(
+							array(
+								'name'=>'token',
+								'label'=>'2-Factor Verification',
+								'type'=>'text',
+								'attributes'=>array(
+									'id'=>'authy-token',
+									'style'=>'background-color:#F2DEDE;'
+								)
+							),
+							array(
+								'name'=>'action',
+								'type'=>'hidden',
+								'value'=>'verify'
+							)
+						)
+					));
+				}else{ 
+					$roles = array(array(
+						'value'=>'user',
+						'label'=>'User'
+					));
+					if($res = query("SELECT name AS value,description AS label FROM ircd.user_role_types")){
+						while($role = $res->fetch_assoc()){
+							array_push($roles,$role);
+						}
+					}
+					array_push($dialogs,array(
+						'id'=>'login-diag',
+						'type'=>'form',
+						'form_id'=>'login',
+						'form_submit_label'=>'Login',
+						'form_fields'=>array(
+							array(
+								'type'=>'custom',
+								'html'=>get_conf('persona-endpoint') != 'none'?"<div><span id='persona-register' class='ui-button ui-widget ui-state-default ui-corner-all' style='overflow:hidden;height:42px;padding:0px 20px 0px 0px;vertical-align:middle;'><img style='height:100%;float:left;' src='img/persona-logo.png'/><span style='display:inline-block;line-height:42px;'>Persona</span></span></div>":''
+							),
+							array(
+								'name'=>'username',
+								'label'=>'Username',
+								'type'=>'text',
+								'value'=>''
+							),
+							array(
+								'name'=>'password',
+								'label'=>'Password',
+								'type'=>'password',
+								'value'=>''
+							),
+							array(
+								'name'=>'type',
+								'label'=>'Type',
+								'type'=>'select',
+								'values'=>$roles
+							),
+							array(
+								'name'=>'action',
+								'type'=>'hidden',
+								'value'=>'login'
+							)
+						)
+					));
+				}
+			?>
+			<div id="dialogs">
+				<?php
+					foreach($dialogs as $k => $diag){
+						echo "<div id='{$diag['id']}'>";
+						switch($diag['type']){
+							case 'form':
+								array_push($diag['form_fields'],array(
+									'type'=>'submit',
+									'value'=>$diag['form_submit_label']
+								));
+								$attributes = array(
+									'id'=>$diag['form_id']
+								);
+								if(isset($diag['autocomplete'])){
+									$attributes['autocomplete'] = $diag['autocomplete'];
+								}
+								echo get_form_html_advanced($attributes,$diag['form_fields']);
+							break;
+						}
+						echo "</div>";
+					}
+				?>
+			</div>
+	</body>
+</html>

+ 9 - 0
site/jquery.ba-resize.min.js

@@ -0,0 +1,9 @@
+/*
+ * jQuery resize event - v1.1 - 3/14/2010
+ * http://benalman.com/projects/jquery-resize-plugin/
+ * 
+ * Copyright (c) 2010 "Cowboy" Ben Alman
+ * Dual licensed under the MIT and GPL licenses.
+ * http://benalman.com/about/license/
+ */
+(function($,h,c){var a=$([]),e=$.resize=$.extend($.resize,{}),i,k="setTimeout",j="resize",d=j+"-special-event",b="delay",f="throttleWindow";e[b]=250;e[f]=true;$.event.special[j]={setup:function(){if(!e[f]&&this[k]){return false}var l=$(this);a=a.add(l);$.data(this,d,{w:l.width(),h:l.height()});if(a.length===1){g()}},teardown:function(){if(!e[f]&&this[k]){return false}var l=$(this);a=a.not(l);l.removeData(d);if(!a.length){clearTimeout(i)}},add:function(l){if(!e[f]&&this[k]){return false}var n;function m(s,o,p){var q=$(this),r=$.data(this,d);r.w=o!==c?o:q.width();r.h=p!==c?p:q.height();n.apply(this,arguments)}if($.isFunction(l)){n=l;return m}else{n=l.handler;l.handler=m}}};function g(){i=h[k](function(){a.each(function(){var n=$(this),m=n.width(),l=n.height(),o=$.data(this,d);if(m!==o.w||l!==o.h){n.trigger(j,[o.w=m,o.h=l])}});g()},e[b])}})(jQuery,this);

+ 12 - 0
site/jquery.timepicker.css

@@ -0,0 +1,12 @@
+/* css for timepicker */
+.ui-timepicker-div .ui-widget-header { margin-bottom: 8px; }
+.ui-timepicker-div dl { text-align: left; }
+.ui-timepicker-div dl dt { float: left; clear:left; padding: 0 0 0 5px; }
+.ui-timepicker-div dl dd { margin: 0 10px 10px 45%; }
+.ui-timepicker-div td { font-size: 90%; }
+.ui-tpicker-grid-label { background: none; border: none; margin: 0; padding: 0; }
+
+.ui-timepicker-rtl{ direction: rtl; }
+.ui-timepicker-rtl dl { text-align: right; padding: 0 5px 0 0; }
+.ui-timepicker-rtl dl dt{ float: right; clear: right; }
+.ui-timepicker-rtl dl dd { margin: 0 45% 10px 10px; }

+ 2145 - 0
site/jquery.timepicker.js

@@ -0,0 +1,2145 @@
+/*! jQuery Timepicker Addon - v1.4.3 - 2013-11-30
+* http://trentrichardson.com/examples/timepicker
+* Copyright (c) 2013 Trent Richardson; Licensed MIT */
+(function ($) {
+
+	/*
+	* Lets not redefine timepicker, Prevent "Uncaught RangeError: Maximum call stack size exceeded"
+	*/
+	$.ui.timepicker = $.ui.timepicker || {};
+	if ($.ui.timepicker.version) {
+		return;
+	}
+
+	/*
+	* Extend jQueryUI, get it started with our version number
+	*/
+	$.extend($.ui, {
+		timepicker: {
+			version: "1.4.3"
+		}
+	});
+
+	/* 
+	* Timepicker manager.
+	* Use the singleton instance of this class, $.timepicker, to interact with the time picker.
+	* Settings for (groups of) time pickers are maintained in an instance object,
+	* allowing multiple different settings on the same page.
+	*/
+	var Timepicker = function () {
+		this.regional = []; // Available regional settings, indexed by language code
+		this.regional[''] = { // Default regional settings
+			currentText: 'Now',
+			closeText: 'Done',
+			amNames: ['AM', 'A'],
+			pmNames: ['PM', 'P'],
+			timeFormat: 'HH:mm',
+			timeSuffix: '',
+			timeOnlyTitle: 'Choose Time',
+			timeText: 'Time',
+			hourText: 'Hour',
+			minuteText: 'Minute',
+			secondText: 'Second',
+			millisecText: 'Millisecond',
+			microsecText: 'Microsecond',
+			timezoneText: 'Time Zone',
+			isRTL: false
+		};
+		this._defaults = { // Global defaults for all the datetime picker instances
+			showButtonPanel: true,
+			timeOnly: false,
+			showHour: null,
+			showMinute: null,
+			showSecond: null,
+			showMillisec: null,
+			showMicrosec: null,
+			showTimezone: null,
+			showTime: true,
+			stepHour: 1,
+			stepMinute: 1,
+			stepSecond: 1,
+			stepMillisec: 1,
+			stepMicrosec: 1,
+			hour: 0,
+			minute: 0,
+			second: 0,
+			millisec: 0,
+			microsec: 0,
+			timezone: null,
+			hourMin: 0,
+			minuteMin: 0,
+			secondMin: 0,
+			millisecMin: 0,
+			microsecMin: 0,
+			hourMax: 23,
+			minuteMax: 59,
+			secondMax: 59,
+			millisecMax: 999,
+			microsecMax: 999,
+			minDateTime: null,
+			maxDateTime: null,
+			onSelect: null,
+			hourGrid: 0,
+			minuteGrid: 0,
+			secondGrid: 0,
+			millisecGrid: 0,
+			microsecGrid: 0,
+			alwaysSetTime: true,
+			separator: ' ',
+			altFieldTimeOnly: true,
+			altTimeFormat: null,
+			altSeparator: null,
+			altTimeSuffix: null,
+			pickerTimeFormat: null,
+			pickerTimeSuffix: null,
+			showTimepicker: true,
+			timezoneList: null,
+			addSliderAccess: false,
+			sliderAccessArgs: null,
+			controlType: 'slider',
+			defaultValue: null,
+			parse: 'strict'
+		};
+		$.extend(this._defaults, this.regional['']);
+	};
+
+	$.extend(Timepicker.prototype, {
+		$input: null,
+		$altInput: null,
+		$timeObj: null,
+		inst: null,
+		hour_slider: null,
+		minute_slider: null,
+		second_slider: null,
+		millisec_slider: null,
+		microsec_slider: null,
+		timezone_select: null,
+		hour: 0,
+		minute: 0,
+		second: 0,
+		millisec: 0,
+		microsec: 0,
+		timezone: null,
+		hourMinOriginal: null,
+		minuteMinOriginal: null,
+		secondMinOriginal: null,
+		millisecMinOriginal: null,
+		microsecMinOriginal: null,
+		hourMaxOriginal: null,
+		minuteMaxOriginal: null,
+		secondMaxOriginal: null,
+		millisecMaxOriginal: null,
+		microsecMaxOriginal: null,
+		ampm: '',
+		formattedDate: '',
+		formattedTime: '',
+		formattedDateTime: '',
+		timezoneList: null,
+		units: ['hour', 'minute', 'second', 'millisec', 'microsec'],
+		support: {},
+		control: null,
+
+		/* 
+		* Override the default settings for all instances of the time picker.
+		* @param  {Object} settings  object - the new settings to use as defaults (anonymous object)
+		* @return {Object} the manager object
+		*/
+		setDefaults: function (settings) {
+			extendRemove(this._defaults, settings || {});
+			return this;
+		},
+
+		/*
+		* Create a new Timepicker instance
+		*/
+		_newInst: function ($input, opts) {
+			var tp_inst = new Timepicker(),
+				inlineSettings = {},
+				fns = {},
+				overrides, i;
+
+			for (var attrName in this._defaults) {
+				if (this._defaults.hasOwnProperty(attrName)) {
+					var attrValue = $input.attr('time:' + attrName);
+					if (attrValue) {
+						try {
+							inlineSettings[attrName] = eval(attrValue);
+						} catch (err) {
+							inlineSettings[attrName] = attrValue;
+						}
+					}
+				}
+			}
+
+			overrides = {
+				beforeShow: function (input, dp_inst) {
+					if ($.isFunction(tp_inst._defaults.evnts.beforeShow)) {
+						return tp_inst._defaults.evnts.beforeShow.call($input[0], input, dp_inst, tp_inst);
+					}
+				},
+				onChangeMonthYear: function (year, month, dp_inst) {
+					// Update the time as well : this prevents the time from disappearing from the $input field.
+					tp_inst._updateDateTime(dp_inst);
+					if ($.isFunction(tp_inst._defaults.evnts.onChangeMonthYear)) {
+						tp_inst._defaults.evnts.onChangeMonthYear.call($input[0], year, month, dp_inst, tp_inst);
+					}
+				},
+				onClose: function (dateText, dp_inst) {
+					if (tp_inst.timeDefined === true && $input.val() !== '') {
+						tp_inst._updateDateTime(dp_inst);
+					}
+					if ($.isFunction(tp_inst._defaults.evnts.onClose)) {
+						tp_inst._defaults.evnts.onClose.call($input[0], dateText, dp_inst, tp_inst);
+					}
+				}
+			};
+			for (i in overrides) {
+				if (overrides.hasOwnProperty(i)) {
+					fns[i] = opts[i] || null;
+				}
+			}
+
+			tp_inst._defaults = $.extend({}, this._defaults, inlineSettings, opts, overrides, {
+				evnts: fns,
+				timepicker: tp_inst // add timepicker as a property of datepicker: $.datepicker._get(dp_inst, 'timepicker');
+			});
+			tp_inst.amNames = $.map(tp_inst._defaults.amNames, function (val) {
+				return val.toUpperCase();
+			});
+			tp_inst.pmNames = $.map(tp_inst._defaults.pmNames, function (val) {
+				return val.toUpperCase();
+			});
+
+			// detect which units are supported
+			tp_inst.support = detectSupport(
+					tp_inst._defaults.timeFormat + 
+					(tp_inst._defaults.pickerTimeFormat ? tp_inst._defaults.pickerTimeFormat : '') +
+					(tp_inst._defaults.altTimeFormat ? tp_inst._defaults.altTimeFormat : ''));
+
+			// controlType is string - key to our this._controls
+			if (typeof(tp_inst._defaults.controlType) === 'string') {
+				if (tp_inst._defaults.controlType === 'slider' && typeof($.ui.slider) === 'undefined') {
+					tp_inst._defaults.controlType = 'select';
+				}
+				tp_inst.control = tp_inst._controls[tp_inst._defaults.controlType];
+			}
+			// controlType is an object and must implement create, options, value methods
+			else {
+				tp_inst.control = tp_inst._defaults.controlType;
+			}
+
+			// prep the timezone options
+			var timezoneList = [-720, -660, -600, -570, -540, -480, -420, -360, -300, -270, -240, -210, -180, -120, -60,
+					0, 60, 120, 180, 210, 240, 270, 300, 330, 345, 360, 390, 420, 480, 525, 540, 570, 600, 630, 660, 690, 720, 765, 780, 840];
+			if (tp_inst._defaults.timezoneList !== null) {
+				timezoneList = tp_inst._defaults.timezoneList;
+			}
+			var tzl = timezoneList.length, tzi = 0, tzv = null;
+			if (tzl > 0 && typeof timezoneList[0] !== 'object') {
+				for (; tzi < tzl; tzi++) {
+					tzv = timezoneList[tzi];
+					timezoneList[tzi] = { value: tzv, label: $.timepicker.timezoneOffsetString(tzv, tp_inst.support.iso8601) };
+				}
+			}
+			tp_inst._defaults.timezoneList = timezoneList;
+
+			// set the default units
+			tp_inst.timezone = tp_inst._defaults.timezone !== null ? $.timepicker.timezoneOffsetNumber(tp_inst._defaults.timezone) :
+							((new Date()).getTimezoneOffset() * -1);
+			tp_inst.hour = tp_inst._defaults.hour < tp_inst._defaults.hourMin ? tp_inst._defaults.hourMin :
+							tp_inst._defaults.hour > tp_inst._defaults.hourMax ? tp_inst._defaults.hourMax : tp_inst._defaults.hour;
+			tp_inst.minute = tp_inst._defaults.minute < tp_inst._defaults.minuteMin ? tp_inst._defaults.minuteMin :
+							tp_inst._defaults.minute > tp_inst._defaults.minuteMax ? tp_inst._defaults.minuteMax : tp_inst._defaults.minute;
+			tp_inst.second = tp_inst._defaults.second < tp_inst._defaults.secondMin ? tp_inst._defaults.secondMin :
+							tp_inst._defaults.second > tp_inst._defaults.secondMax ? tp_inst._defaults.secondMax : tp_inst._defaults.second;
+			tp_inst.millisec = tp_inst._defaults.millisec < tp_inst._defaults.millisecMin ? tp_inst._defaults.millisecMin :
+							tp_inst._defaults.millisec > tp_inst._defaults.millisecMax ? tp_inst._defaults.millisecMax : tp_inst._defaults.millisec;
+			tp_inst.microsec = tp_inst._defaults.microsec < tp_inst._defaults.microsecMin ? tp_inst._defaults.microsecMin :
+							tp_inst._defaults.microsec > tp_inst._defaults.microsecMax ? tp_inst._defaults.microsecMax : tp_inst._defaults.microsec;
+			tp_inst.ampm = '';
+			tp_inst.$input = $input;
+
+			if (tp_inst._defaults.altField) {
+				tp_inst.$altInput = $(tp_inst._defaults.altField).css({
+					cursor: 'pointer'
+				}).focus(function () {
+					$input.trigger("focus");
+				});
+			}
+
+			if (tp_inst._defaults.minDate === 0 || tp_inst._defaults.minDateTime === 0) {
+				tp_inst._defaults.minDate = new Date();
+			}
+			if (tp_inst._defaults.maxDate === 0 || tp_inst._defaults.maxDateTime === 0) {
+				tp_inst._defaults.maxDate = new Date();
+			}
+
+			// datepicker needs minDate/maxDate, timepicker needs minDateTime/maxDateTime..
+			if (tp_inst._defaults.minDate !== undefined && tp_inst._defaults.minDate instanceof Date) {
+				tp_inst._defaults.minDateTime = new Date(tp_inst._defaults.minDate.getTime());
+			}
+			if (tp_inst._defaults.minDateTime !== undefined && tp_inst._defaults.minDateTime instanceof Date) {
+				tp_inst._defaults.minDate = new Date(tp_inst._defaults.minDateTime.getTime());
+			}
+			if (tp_inst._defaults.maxDate !== undefined && tp_inst._defaults.maxDate instanceof Date) {
+				tp_inst._defaults.maxDateTime = new Date(tp_inst._defaults.maxDate.getTime());
+			}
+			if (tp_inst._defaults.maxDateTime !== undefined && tp_inst._defaults.maxDateTime instanceof Date) {
+				tp_inst._defaults.maxDate = new Date(tp_inst._defaults.maxDateTime.getTime());
+			}
+			tp_inst.$input.bind('focus', function () {
+				tp_inst._onFocus();
+			});
+
+			return tp_inst;
+		},
+
+		/*
+		* add our sliders to the calendar
+		*/
+		_addTimePicker: function (dp_inst) {
+			var currDT = (this.$altInput && this._defaults.altFieldTimeOnly) ? this.$input.val() + ' ' + this.$altInput.val() : this.$input.val();
+
+			this.timeDefined = this._parseTime(currDT);
+			this._limitMinMaxDateTime(dp_inst, false);
+			this._injectTimePicker();
+		},
+
+		/*
+		* parse the time string from input value or _setTime
+		*/
+		_parseTime: function (timeString, withDate) {
+			if (!this.inst) {
+				this.inst = $.datepicker._getInst(this.$input[0]);
+			}
+
+			if (withDate || !this._defaults.timeOnly) {
+				var dp_dateFormat = $.datepicker._get(this.inst, 'dateFormat');
+				try {
+					var parseRes = parseDateTimeInternal(dp_dateFormat, this._defaults.timeFormat, timeString, $.datepicker._getFormatConfig(this.inst), this._defaults);
+					if (!parseRes.timeObj) {
+						return false;
+					}
+					$.extend(this, parseRes.timeObj);
+				} catch (err) {
+					$.timepicker.log("Error parsing the date/time string: " + err +
+									"\ndate/time string = " + timeString +
+									"\ntimeFormat = " + this._defaults.timeFormat +
+									"\ndateFormat = " + dp_dateFormat);
+					return false;
+				}
+				return true;
+			} else {
+				var timeObj = $.datepicker.parseTime(this._defaults.timeFormat, timeString, this._defaults);
+				if (!timeObj) {
+					return false;
+				}
+				$.extend(this, timeObj);
+				return true;
+			}
+		},
+
+		/*
+		* generate and inject html for timepicker into ui datepicker
+		*/
+		_injectTimePicker: function () {
+			var $dp = this.inst.dpDiv,
+				o = this.inst.settings,
+				tp_inst = this,
+				litem = '',
+				uitem = '',
+				show = null,
+				max = {},
+				gridSize = {},
+				size = null,
+				i = 0,
+				l = 0;
+
+			// Prevent displaying twice
+			if ($dp.find("div.ui-timepicker-div").length === 0 && o.showTimepicker) {
+				var noDisplay = ' style="display:none;"',
+					html = '<div class="ui-timepicker-div' + (o.isRTL ? ' ui-timepicker-rtl' : '') + '"><dl>' + '<dt class="ui_tpicker_time_label"' + ((o.showTime) ? '' : noDisplay) + '>' + o.timeText + '</dt>' +
+								'<dd class="ui_tpicker_time"' + ((o.showTime) ? '' : noDisplay) + '></dd>';
+
+				// Create the markup
+				for (i = 0, l = this.units.length; i < l; i++) {
+					litem = this.units[i];
+					uitem = litem.substr(0, 1).toUpperCase() + litem.substr(1);
+					show = o['show' + uitem] !== null ? o['show' + uitem] : this.support[litem];
+
+					// Added by Peter Medeiros:
+					// - Figure out what the hour/minute/second max should be based on the step values.
+					// - Example: if stepMinute is 15, then minMax is 45.
+					max[litem] = parseInt((o[litem + 'Max'] - ((o[litem + 'Max'] - o[litem + 'Min']) % o['step' + uitem])), 10);
+					gridSize[litem] = 0;
+
+					html += '<dt class="ui_tpicker_' + litem + '_label"' + (show ? '' : noDisplay) + '>' + o[litem + 'Text'] + '</dt>' +
+								'<dd class="ui_tpicker_' + litem + '"><div class="ui_tpicker_' + litem + '_slider"' + (show ? '' : noDisplay) + '></div>';
+
+					if (show && o[litem + 'Grid'] > 0) {
+						html += '<div style="padding-left: 1px"><table class="ui-tpicker-grid-label"><tr>';
+
+						if (litem === 'hour') {
+							for (var h = o[litem + 'Min']; h <= max[litem]; h += parseInt(o[litem + 'Grid'], 10)) {
+								gridSize[litem]++;
+								var tmph = $.datepicker.formatTime(this.support.ampm ? 'hht' : 'HH', {hour: h}, o);
+								html += '<td data-for="' + litem + '">' + tmph + '</td>';
+							}
+						}
+						else {
+							for (var m = o[litem + 'Min']; m <= max[litem]; m += parseInt(o[litem + 'Grid'], 10)) {
+								gridSize[litem]++;
+								html += '<td data-for="' + litem + '">' + ((m < 10) ? '0' : '') + m + '</td>';
+							}
+						}
+
+						html += '</tr></table></div>';
+					}
+					html += '</dd>';
+				}
+				
+				// Timezone
+				var showTz = o.showTimezone !== null ? o.showTimezone : this.support.timezone;
+				html += '<dt class="ui_tpicker_timezone_label"' + (showTz ? '' : noDisplay) + '>' + o.timezoneText + '</dt>';
+				html += '<dd class="ui_tpicker_timezone" ' + (showTz ? '' : noDisplay) + '></dd>';
+
+				// Create the elements from string
+				html += '</dl></div>';
+				var $tp = $(html);
+
+				// if we only want time picker...
+				if (o.timeOnly === true) {
+					$tp.prepend('<div class="ui-widget-header ui-helper-clearfix ui-corner-all">' + '<div class="ui-datepicker-title">' + o.timeOnlyTitle + '</div>' + '</div>');
+					$dp.find('.ui-datepicker-header, .ui-datepicker-calendar').hide();
+				}
+				
+				// add sliders, adjust grids, add events
+				for (i = 0, l = tp_inst.units.length; i < l; i++) {
+					litem = tp_inst.units[i];
+					uitem = litem.substr(0, 1).toUpperCase() + litem.substr(1);
+					show = o['show' + uitem] !== null ? o['show' + uitem] : this.support[litem];
+
+					// add the slider
+					tp_inst[litem + '_slider'] = tp_inst.control.create(tp_inst, $tp.find('.ui_tpicker_' + litem + '_slider'), litem, tp_inst[litem], o[litem + 'Min'], max[litem], o['step' + uitem]);
+
+					// adjust the grid and add click event
+					if (show && o[litem + 'Grid'] > 0) {
+						size = 100 * gridSize[litem] * o[litem + 'Grid'] / (max[litem] - o[litem + 'Min']);
+						$tp.find('.ui_tpicker_' + litem + ' table').css({
+							width: size + "%",
+							marginLeft: o.isRTL ? '0' : ((size / (-2 * gridSize[litem])) + "%"),
+							marginRight: o.isRTL ? ((size / (-2 * gridSize[litem])) + "%") : '0',
+							borderCollapse: 'collapse'
+						}).find("td").click(function (e) {
+								var $t = $(this),
+									h = $t.html(),
+									n = parseInt(h.replace(/[^0-9]/g), 10),
+									ap = h.replace(/[^apm]/ig),
+									f = $t.data('for'); // loses scope, so we use data-for
+
+								if (f === 'hour') {
+									if (ap.indexOf('p') !== -1 && n < 12) {
+										n += 12;
+									}
+									else {
+										if (ap.indexOf('a') !== -1 && n === 12) {
+											n = 0;
+										}
+									}
+								}
+								
+								tp_inst.control.value(tp_inst, tp_inst[f + '_slider'], litem, n);
+
+								tp_inst._onTimeChange();
+								tp_inst._onSelectHandler();
+							}).css({
+								cursor: 'pointer',
+								width: (100 / gridSize[litem]) + '%',
+								textAlign: 'center',
+								overflow: 'hidden'
+							});
+					} // end if grid > 0
+				} // end for loop
+
+				// Add timezone options
+				this.timezone_select = $tp.find('.ui_tpicker_timezone').append('<select></select>').find("select");
+				$.fn.append.apply(this.timezone_select,
+				$.map(o.timezoneList, function (val, idx) {
+					return $("<option />").val(typeof val === "object" ? val.value : val).text(typeof val === "object" ? val.label : val);
+				}));
+				if (typeof(this.timezone) !== "undefined" && this.timezone !== null && this.timezone !== "") {
+					var local_timezone = (new Date(this.inst.selectedYear, this.inst.selectedMonth, this.inst.selectedDay, 12)).getTimezoneOffset() * -1;
+					if (local_timezone === this.timezone) {
+						selectLocalTimezone(tp_inst);
+					} else {
+						this.timezone_select.val(this.timezone);
+					}
+				} else {
+					if (typeof(this.hour) !== "undefined" && this.hour !== null && this.hour !== "") {
+						this.timezone_select.val(o.timezone);
+					} else {
+						selectLocalTimezone(tp_inst);
+					}
+				}
+				this.timezone_select.change(function () {
+					tp_inst._onTimeChange();
+					tp_inst._onSelectHandler();
+				});
+				// End timezone options
+				
+				// inject timepicker into datepicker
+				var $buttonPanel = $dp.find('.ui-datepicker-buttonpane');
+				if ($buttonPanel.length) {
+					$buttonPanel.before($tp);
+				} else {
+					$dp.append($tp);
+				}
+
+				this.$timeObj = $tp.find('.ui_tpicker_time');
+
+				if (this.inst !== null) {
+					var timeDefined = this.timeDefined;
+					this._onTimeChange();
+					this.timeDefined = timeDefined;
+				}
+
+				// slideAccess integration: http://trentrichardson.com/2011/11/11/jquery-ui-sliders-and-touch-accessibility/
+				if (this._defaults.addSliderAccess) {
+					var sliderAccessArgs = this._defaults.sliderAccessArgs,
+						rtl = this._defaults.isRTL;
+					sliderAccessArgs.isRTL = rtl;
+						
+					setTimeout(function () { // fix for inline mode
+						if ($tp.find('.ui-slider-access').length === 0) {
+							$tp.find('.ui-slider:visible').sliderAccess(sliderAccessArgs);
+
+							// fix any grids since sliders are shorter
+							var sliderAccessWidth = $tp.find('.ui-slider-access:eq(0)').outerWidth(true);
+							if (sliderAccessWidth) {
+								$tp.find('table:visible').each(function () {
+									var $g = $(this),
+										oldWidth = $g.outerWidth(),
+										oldMarginLeft = $g.css(rtl ? 'marginRight' : 'marginLeft').toString().replace('%', ''),
+										newWidth = oldWidth - sliderAccessWidth,
+										newMarginLeft = ((oldMarginLeft * newWidth) / oldWidth) + '%',
+										css = { width: newWidth, marginRight: 0, marginLeft: 0 };
+									css[rtl ? 'marginRight' : 'marginLeft'] = newMarginLeft;
+									$g.css(css);
+								});
+							}
+						}
+					}, 10);
+				}
+				// end slideAccess integration
+
+				tp_inst._limitMinMaxDateTime(this.inst, true);
+			}
+		},
+
+		/*
+		* This function tries to limit the ability to go outside the
+		* min/max date range
+		*/
+		_limitMinMaxDateTime: function (dp_inst, adjustSliders) {
+			var o = this._defaults,
+				dp_date = new Date(dp_inst.selectedYear, dp_inst.selectedMonth, dp_inst.selectedDay);
+
+			if (!this._defaults.showTimepicker) {
+				return;
+			} // No time so nothing to check here
+
+			if ($.datepicker._get(dp_inst, 'minDateTime') !== null && $.datepicker._get(dp_inst, 'minDateTime') !== undefined && dp_date) {
+				var minDateTime = $.datepicker._get(dp_inst, 'minDateTime'),
+					minDateTimeDate = new Date(minDateTime.getFullYear(), minDateTime.getMonth(), minDateTime.getDate(), 0, 0, 0, 0);
+
+				if (this.hourMinOriginal === null || this.minuteMinOriginal === null || this.secondMinOriginal === null || this.millisecMinOriginal === null || this.microsecMinOriginal === null) {
+					this.hourMinOriginal = o.hourMin;
+					this.minuteMinOriginal = o.minuteMin;
+					this.secondMinOriginal = o.secondMin;
+					this.millisecMinOriginal = o.millisecMin;
+					this.microsecMinOriginal = o.microsecMin;
+				}
+
+				if (dp_inst.settings.timeOnly || minDateTimeDate.getTime() === dp_date.getTime()) {
+					this._defaults.hourMin = minDateTime.getHours();
+					if (this.hour <= this._defaults.hourMin) {
+						this.hour = this._defaults.hourMin;
+						this._defaults.minuteMin = minDateTime.getMinutes();
+						if (this.minute <= this._defaults.minuteMin) {
+							this.minute = this._defaults.minuteMin;
+							this._defaults.secondMin = minDateTime.getSeconds();
+							if (this.second <= this._defaults.secondMin) {
+								this.second = this._defaults.secondMin;
+								this._defaults.millisecMin = minDateTime.getMilliseconds();
+								if (this.millisec <= this._defaults.millisecMin) {
+									this.millisec = this._defaults.millisecMin;
+									this._defaults.microsecMin = minDateTime.getMicroseconds();
+								} else {
+									if (this.microsec < this._defaults.microsecMin) {
+										this.microsec = this._defaults.microsecMin;
+									}
+									this._defaults.microsecMin = this.microsecMinOriginal;
+								}
+							} else {
+								this._defaults.millisecMin = this.millisecMinOriginal;
+								this._defaults.microsecMin = this.microsecMinOriginal;
+							}
+						} else {
+							this._defaults.secondMin = this.secondMinOriginal;
+							this._defaults.millisecMin = this.millisecMinOriginal;
+							this._defaults.microsecMin = this.microsecMinOriginal;
+						}
+					} else {
+						this._defaults.minuteMin = this.minuteMinOriginal;
+						this._defaults.secondMin = this.secondMinOriginal;
+						this._defaults.millisecMin = this.millisecMinOriginal;
+						this._defaults.microsecMin = this.microsecMinOriginal;
+					}
+				} else {
+					this._defaults.hourMin = this.hourMinOriginal;
+					this._defaults.minuteMin = this.minuteMinOriginal;
+					this._defaults.secondMin = this.secondMinOriginal;
+					this._defaults.millisecMin = this.millisecMinOriginal;
+					this._defaults.microsecMin = this.microsecMinOriginal;
+				}
+			}
+
+			if ($.datepicker._get(dp_inst, 'maxDateTime') !== null && $.datepicker._get(dp_inst, 'maxDateTime') !== undefined && dp_date) {
+				var maxDateTime = $.datepicker._get(dp_inst, 'maxDateTime'),
+					maxDateTimeDate = new Date(maxDateTime.getFullYear(), maxDateTime.getMonth(), maxDateTime.getDate(), 0, 0, 0, 0);
+
+				if (this.hourMaxOriginal === null || this.minuteMaxOriginal === null || this.secondMaxOriginal === null || this.millisecMaxOriginal === null) {
+					this.hourMaxOriginal = o.hourMax;
+					this.minuteMaxOriginal = o.minuteMax;
+					this.secondMaxOriginal = o.secondMax;
+					this.millisecMaxOriginal = o.millisecMax;
+					this.microsecMaxOriginal = o.microsecMax;
+				}
+
+				if (dp_inst.settings.timeOnly || maxDateTimeDate.getTime() === dp_date.getTime()) {
+					this._defaults.hourMax = maxDateTime.getHours();
+					if (this.hour >= this._defaults.hourMax) {
+						this.hour = this._defaults.hourMax;
+						this._defaults.minuteMax = maxDateTime.getMinutes();
+						if (this.minute >= this._defaults.minuteMax) {
+							this.minute = this._defaults.minuteMax;
+							this._defaults.secondMax = maxDateTime.getSeconds();
+							if (this.second >= this._defaults.secondMax) {
+								this.second = this._defaults.secondMax;
+								this._defaults.millisecMax = maxDateTime.getMilliseconds();
+								if (this.millisec >= this._defaults.millisecMax) {
+									this.millisec = this._defaults.millisecMax;
+									this._defaults.microsecMax = maxDateTime.getMicroseconds();
+								} else {
+									if (this.microsec > this._defaults.microsecMax) {
+										this.microsec = this._defaults.microsecMax;
+									}
+									this._defaults.microsecMax = this.microsecMaxOriginal;
+								}
+							} else {
+								this._defaults.millisecMax = this.millisecMaxOriginal;
+								this._defaults.microsecMax = this.microsecMaxOriginal;
+							}
+						} else {
+							this._defaults.secondMax = this.secondMaxOriginal;
+							this._defaults.millisecMax = this.millisecMaxOriginal;
+							this._defaults.microsecMax = this.microsecMaxOriginal;
+						}
+					} else {
+						this._defaults.minuteMax = this.minuteMaxOriginal;
+						this._defaults.secondMax = this.secondMaxOriginal;
+						this._defaults.millisecMax = this.millisecMaxOriginal;
+						this._defaults.microsecMax = this.microsecMaxOriginal;
+					}
+				} else {
+					this._defaults.hourMax = this.hourMaxOriginal;
+					this._defaults.minuteMax = this.minuteMaxOriginal;
+					this._defaults.secondMax = this.secondMaxOriginal;
+					this._defaults.millisecMax = this.millisecMaxOriginal;
+					this._defaults.microsecMax = this.microsecMaxOriginal;
+				}
+			}
+
+			if (adjustSliders !== undefined && adjustSliders === true) {
+				var hourMax = parseInt((this._defaults.hourMax - ((this._defaults.hourMax - this._defaults.hourMin) % this._defaults.stepHour)), 10),
+					minMax = parseInt((this._defaults.minuteMax - ((this._defaults.minuteMax - this._defaults.minuteMin) % this._defaults.stepMinute)), 10),
+					secMax = parseInt((this._defaults.secondMax - ((this._defaults.secondMax - this._defaults.secondMin) % this._defaults.stepSecond)), 10),
+					millisecMax = parseInt((this._defaults.millisecMax - ((this._defaults.millisecMax - this._defaults.millisecMin) % this._defaults.stepMillisec)), 10),
+					microsecMax = parseInt((this._defaults.microsecMax - ((this._defaults.microsecMax - this._defaults.microsecMin) % this._defaults.stepMicrosec)), 10);
+
+				if (this.hour_slider) {
+					this.control.options(this, this.hour_slider, 'hour', { min: this._defaults.hourMin, max: hourMax });
+					this.control.value(this, this.hour_slider, 'hour', this.hour - (this.hour % this._defaults.stepHour));
+				}
+				if (this.minute_slider) {
+					this.control.options(this, this.minute_slider, 'minute', { min: this._defaults.minuteMin, max: minMax });
+					this.control.value(this, this.minute_slider, 'minute', this.minute - (this.minute % this._defaults.stepMinute));
+				}
+				if (this.second_slider) {
+					this.control.options(this, this.second_slider, 'second', { min: this._defaults.secondMin, max: secMax });
+					this.control.value(this, this.second_slider, 'second', this.second - (this.second % this._defaults.stepSecond));
+				}
+				if (this.millisec_slider) {
+					this.control.options(this, this.millisec_slider, 'millisec', { min: this._defaults.millisecMin, max: millisecMax });
+					this.control.value(this, this.millisec_slider, 'millisec', this.millisec - (this.millisec % this._defaults.stepMillisec));
+				}
+				if (this.microsec_slider) {
+					this.control.options(this, this.microsec_slider, 'microsec', { min: this._defaults.microsecMin, max: microsecMax });
+					this.control.value(this, this.microsec_slider, 'microsec', this.microsec - (this.microsec % this._defaults.stepMicrosec));
+				}
+			}
+
+		},
+
+		/*
+		* when a slider moves, set the internal time...
+		* on time change is also called when the time is updated in the text field
+		*/
+		_onTimeChange: function () {
+			if (!this._defaults.showTimepicker) {
+                                return;
+			}
+			var hour = (this.hour_slider) ? this.control.value(this, this.hour_slider, 'hour') : false,
+				minute = (this.minute_slider) ? this.control.value(this, this.minute_slider, 'minute') : false,
+				second = (this.second_slider) ? this.control.value(this, this.second_slider, 'second') : false,
+				millisec = (this.millisec_slider) ? this.control.value(this, this.millisec_slider, 'millisec') : false,
+				microsec = (this.microsec_slider) ? this.control.value(this, this.microsec_slider, 'microsec') : false,
+				timezone = (this.timezone_select) ? this.timezone_select.val() : false,
+				o = this._defaults,
+				pickerTimeFormat = o.pickerTimeFormat || o.timeFormat,
+				pickerTimeSuffix = o.pickerTimeSuffix || o.timeSuffix;
+
+			if (typeof(hour) === 'object') {
+				hour = false;
+			}
+			if (typeof(minute) === 'object') {
+				minute = false;
+			}
+			if (typeof(second) === 'object') {
+				second = false;
+			}
+			if (typeof(millisec) === 'object') {
+				millisec = false;
+			}
+			if (typeof(microsec) === 'object') {
+				microsec = false;
+			}
+			if (typeof(timezone) === 'object') {
+				timezone = false;
+			}
+
+			if (hour !== false) {
+				hour = parseInt(hour, 10);
+			}
+			if (minute !== false) {
+				minute = parseInt(minute, 10);
+			}
+			if (second !== false) {
+				second = parseInt(second, 10);
+			}
+			if (millisec !== false) {
+				millisec = parseInt(millisec, 10);
+			}
+			if (microsec !== false) {
+				microsec = parseInt(microsec, 10);
+			}
+			if (timezone !== false) {
+				timezone = timezone.toString();
+			}
+
+			var ampm = o[hour < 12 ? 'amNames' : 'pmNames'][0];
+
+			// If the update was done in the input field, the input field should not be updated.
+			// If the update was done using the sliders, update the input field.
+			var hasChanged = (
+						hour !== parseInt(this.hour,10) || // sliders should all be numeric
+						minute !== parseInt(this.minute,10) || 
+						second !== parseInt(this.second,10) || 
+						millisec !== parseInt(this.millisec,10) || 
+						microsec !== parseInt(this.microsec,10) || 
+						(this.ampm.length > 0 && (hour < 12) !== ($.inArray(this.ampm.toUpperCase(), this.amNames) !== -1)) || 
+						(this.timezone !== null && timezone !== this.timezone.toString()) // could be numeric or "EST" format, so use toString()
+					);
+
+			if (hasChanged) {
+
+				if (hour !== false) {
+					this.hour = hour;
+				}
+				if (minute !== false) {
+					this.minute = minute;
+				}
+				if (second !== false) {
+					this.second = second;
+				}
+				if (millisec !== false) {
+					this.millisec = millisec;
+				}
+				if (microsec !== false) {
+					this.microsec = microsec;
+				}
+				if (timezone !== false) {
+					this.timezone = timezone;
+				}
+
+				if (!this.inst) {
+					this.inst = $.datepicker._getInst(this.$input[0]);
+				}
+
+				this._limitMinMaxDateTime(this.inst, true);
+			}
+			if (this.support.ampm) {
+				this.ampm = ampm;
+			}
+
+			// Updates the time within the timepicker
+			this.formattedTime = $.datepicker.formatTime(o.timeFormat, this, o);
+			if (this.$timeObj) {
+				if (pickerTimeFormat === o.timeFormat) {
+					this.$timeObj.text(this.formattedTime + pickerTimeSuffix);
+				}
+				else {
+					this.$timeObj.text($.datepicker.formatTime(pickerTimeFormat, this, o) + pickerTimeSuffix);
+				}
+			}
+
+			this.timeDefined = true;
+			if (hasChanged) {
+				this._updateDateTime();
+				this.$input.focus();
+			}
+		},
+
+		/*
+		* call custom onSelect.
+		* bind to sliders slidestop, and grid click.
+		*/
+		_onSelectHandler: function () {
+			var onSelect = this._defaults.onSelect || this.inst.settings.onSelect;
+			var inputEl = this.$input ? this.$input[0] : null;
+			if (onSelect && inputEl) {
+				onSelect.apply(inputEl, [this.formattedDateTime, this]);
+			}
+		},
+
+		/*
+		* update our input with the new date time..
+		*/
+		_updateDateTime: function (dp_inst) {
+			dp_inst = this.inst || dp_inst;
+			var dtTmp = (dp_inst.currentYear > 0? 
+							new Date(dp_inst.currentYear, dp_inst.currentMonth, dp_inst.currentDay) : 
+							new Date(dp_inst.selectedYear, dp_inst.selectedMonth, dp_inst.selectedDay)),
+				dt = $.datepicker._daylightSavingAdjust(dtTmp),
+				//dt = $.datepicker._daylightSavingAdjust(new Date(dp_inst.selectedYear, dp_inst.selectedMonth, dp_inst.selectedDay)),
+				//dt = $.datepicker._daylightSavingAdjust(new Date(dp_inst.currentYear, dp_inst.currentMonth, dp_inst.currentDay)),
+				dateFmt = $.datepicker._get(dp_inst, 'dateFormat'),
+				formatCfg = $.datepicker._getFormatConfig(dp_inst),
+				timeAvailable = dt !== null && this.timeDefined;
+			this.formattedDate = $.datepicker.formatDate(dateFmt, (dt === null ? new Date() : dt), formatCfg);
+			var formattedDateTime = this.formattedDate;
+			
+			// if a slider was changed but datepicker doesn't have a value yet, set it
+			if (dp_inst.lastVal === "") {
+                dp_inst.currentYear = dp_inst.selectedYear;
+                dp_inst.currentMonth = dp_inst.selectedMonth;
+                dp_inst.currentDay = dp_inst.selectedDay;
+            }
+
+			/*
+			* remove following lines to force every changes in date picker to change the input value
+			* Bug descriptions: when an input field has a default value, and click on the field to pop up the date picker. 
+			* If the user manually empty the value in the input field, the date picker will never change selected value.
+			*/
+			//if (dp_inst.lastVal !== undefined && (dp_inst.lastVal.length > 0 && this.$input.val().length === 0)) {
+			//	return;
+			//}
+
+			if (this._defaults.timeOnly === true) {
+				formattedDateTime = this.formattedTime;
+			} else if (this._defaults.timeOnly !== true && (this._defaults.alwaysSetTime || timeAvailable)) {
+				formattedDateTime += this._defaults.separator + this.formattedTime + this._defaults.timeSuffix;
+			}
+
+			this.formattedDateTime = formattedDateTime;
+
+			if (!this._defaults.showTimepicker) {
+				this.$input.val(this.formattedDate);
+			} else if (this.$altInput && this._defaults.timeOnly === false && this._defaults.altFieldTimeOnly === true) {
+				this.$altInput.val(this.formattedTime);
+				this.$input.val(this.formattedDate);
+			} else if (this.$altInput) {
+				this.$input.val(formattedDateTime);
+				var altFormattedDateTime = '',
+					altSeparator = this._defaults.altSeparator ? this._defaults.altSeparator : this._defaults.separator,
+					altTimeSuffix = this._defaults.altTimeSuffix ? this._defaults.altTimeSuffix : this._defaults.timeSuffix;
+				
+				if (!this._defaults.timeOnly) {
+					if (this._defaults.altFormat) {
+						altFormattedDateTime = $.datepicker.formatDate(this._defaults.altFormat, (dt === null ? new Date() : dt), formatCfg);
+					}
+					else {
+						altFormattedDateTime = this.formattedDate;
+					}
+
+					if (altFormattedDateTime) {
+						altFormattedDateTime += altSeparator;
+					}
+				}
+
+				if (this._defaults.altTimeFormat) {
+					altFormattedDateTime += $.datepicker.formatTime(this._defaults.altTimeFormat, this, this._defaults) + altTimeSuffix;
+				}
+				else {
+					altFormattedDateTime += this.formattedTime + altTimeSuffix;
+				}
+				this.$altInput.val(altFormattedDateTime);
+			} else {
+				this.$input.val(formattedDateTime);
+			}
+
+			this.$input.trigger("change");
+		},
+
+		_onFocus: function () {
+			if (!this.$input.val() && this._defaults.defaultValue) {
+				this.$input.val(this._defaults.defaultValue);
+				var inst = $.datepicker._getInst(this.$input.get(0)),
+					tp_inst = $.datepicker._get(inst, 'timepicker');
+				if (tp_inst) {
+					if (tp_inst._defaults.timeOnly && (inst.input.val() !== inst.lastVal)) {
+						try {
+							$.datepicker._updateDatepicker(inst);
+						} catch (err) {
+							$.timepicker.log(err);
+						}
+					}
+				}
+			}
+		},
+
+		/*
+		* Small abstraction to control types
+		* We can add more, just be sure to follow the pattern: create, options, value
+		*/
+		_controls: {
+			// slider methods
+			slider: {
+				create: function (tp_inst, obj, unit, val, min, max, step) {
+					var rtl = tp_inst._defaults.isRTL; // if rtl go -60->0 instead of 0->60
+					return obj.prop('slide', null).slider({
+						orientation: "horizontal",
+						value: rtl ? val * -1 : val,
+						min: rtl ? max * -1 : min,
+						max: rtl ? min * -1 : max,
+						step: step,
+						slide: function (event, ui) {
+							tp_inst.control.value(tp_inst, $(this), unit, rtl ? ui.value * -1 : ui.value);
+							tp_inst._onTimeChange();
+						},
+						stop: function (event, ui) {
+							tp_inst._onSelectHandler();
+						}
+					});	
+				},
+				options: function (tp_inst, obj, unit, opts, val) {
+					if (tp_inst._defaults.isRTL) {
+						if (typeof(opts) === 'string') {
+							if (opts === 'min' || opts === 'max') {
+								if (val !== undefined) {
+									return obj.slider(opts, val * -1);
+								}
+								return Math.abs(obj.slider(opts));
+							}
+							return obj.slider(opts);
+						}
+						var min = opts.min, 
+							max = opts.max;
+						opts.min = opts.max = null;
+						if (min !== undefined) {
+							opts.max = min * -1;
+						}
+						if (max !== undefined) {
+							opts.min = max * -1;
+						}
+						return obj.slider(opts);
+					}
+					if (typeof(opts) === 'string' && val !== undefined) {
+						return obj.slider(opts, val);
+					}
+					return obj.slider(opts);
+				},
+				value: function (tp_inst, obj, unit, val) {
+					if (tp_inst._defaults.isRTL) {
+						if (val !== undefined) {
+							return obj.slider('value', val * -1);
+						}
+						return Math.abs(obj.slider('value'));
+					}
+					if (val !== undefined) {
+						return obj.slider('value', val);
+					}
+					return obj.slider('value');
+				}
+			},
+			// select methods
+			select: {
+				create: function (tp_inst, obj, unit, val, min, max, step) {
+					var sel = '<select class="ui-timepicker-select" data-unit="' + unit + '" data-min="' + min + '" data-max="' + max + '" data-step="' + step + '">',
+						format = tp_inst._defaults.pickerTimeFormat || tp_inst._defaults.timeFormat;
+
+					for (var i = min; i <= max; i += step) {
+						sel += '<option value="' + i + '"' + (i === val ? ' selected' : '') + '>';
+						if (unit === 'hour') {
+							sel += $.datepicker.formatTime($.trim(format.replace(/[^ht ]/ig, '')), {hour: i}, tp_inst._defaults);
+						}
+						else if (unit === 'millisec' || unit === 'microsec' || i >= 10) { sel += i; }
+						else {sel += '0' + i.toString(); }
+						sel += '</option>';
+					}
+					sel += '</select>';
+
+					obj.children('select').remove();
+
+					$(sel).appendTo(obj).change(function (e) {
+						tp_inst._onTimeChange();
+						tp_inst._onSelectHandler();
+					});
+
+					return obj;
+				},
+				options: function (tp_inst, obj, unit, opts, val) {
+					var o = {},
+						$t = obj.children('select');
+					if (typeof(opts) === 'string') {
+						if (val === undefined) {
+							return $t.data(opts);
+						}
+						o[opts] = val;	
+					}
+					else { o = opts; }
+					return tp_inst.control.create(tp_inst, obj, $t.data('unit'), $t.val(), o.min || $t.data('min'), o.max || $t.data('max'), o.step || $t.data('step'));
+				},
+				value: function (tp_inst, obj, unit, val) {
+					var $t = obj.children('select');
+					if (val !== undefined) {
+						return $t.val(val);
+					}
+					return $t.val();
+				}
+			}
+		} // end _controls
+
+	});
+
+	$.fn.extend({
+		/*
+		* shorthand just to use timepicker.
+		*/
+		timepicker: function (o) {
+			o = o || {};
+			var tmp_args = Array.prototype.slice.call(arguments);
+
+			if (typeof o === 'object') {
+				tmp_args[0] = $.extend(o, {
+					timeOnly: true
+				});
+			}
+
+			return $(this).each(function () {
+				$.fn.datetimepicker.apply($(this), tmp_args);
+			});
+		},
+
+		/*
+		* extend timepicker to datepicker
+		*/
+		datetimepicker: function (o) {
+			o = o || {};
+			var tmp_args = arguments;
+
+			if (typeof(o) === 'string') {
+				if (o === 'getDate') {
+					return $.fn.datepicker.apply($(this[0]), tmp_args);
+				} else {
+					return this.each(function () {
+						var $t = $(this);
+						$t.datepicker.apply($t, tmp_args);
+					});
+				}
+			} else {
+				return this.each(function () {
+					var $t = $(this);
+					$t.datepicker($.timepicker._newInst($t, o)._defaults);
+				});
+			}
+		}
+	});
+
+	/*
+	* Public Utility to parse date and time
+	*/
+	$.datepicker.parseDateTime = function (dateFormat, timeFormat, dateTimeString, dateSettings, timeSettings) {
+		var parseRes = parseDateTimeInternal(dateFormat, timeFormat, dateTimeString, dateSettings, timeSettings);
+		if (parseRes.timeObj) {
+			var t = parseRes.timeObj;
+			parseRes.date.setHours(t.hour, t.minute, t.second, t.millisec);
+			parseRes.date.setMicroseconds(t.microsec);
+		}
+
+		return parseRes.date;
+	};
+
+	/*
+	* Public utility to parse time
+	*/
+	$.datepicker.parseTime = function (timeFormat, timeString, options) {
+		var o = extendRemove(extendRemove({}, $.timepicker._defaults), options || {}),
+			iso8601 = (timeFormat.replace(/\'.*?\'/g, '').indexOf('Z') !== -1);
+
+		// Strict parse requires the timeString to match the timeFormat exactly
+		var strictParse = function (f, s, o) {
+
+			// pattern for standard and localized AM/PM markers
+			var getPatternAmpm = function (amNames, pmNames) {
+				var markers = [];
+				if (amNames) {
+					$.merge(markers, amNames);
+				}
+				if (pmNames) {
+					$.merge(markers, pmNames);
+				}
+				markers = $.map(markers, function (val) {
+					return val.replace(/[.*+?|()\[\]{}\\]/g, '\\$&');
+				});
+				return '(' + markers.join('|') + ')?';
+			};
+
+			// figure out position of time elements.. cause js cant do named captures
+			var getFormatPositions = function (timeFormat) {
+				var finds = timeFormat.toLowerCase().match(/(h{1,2}|m{1,2}|s{1,2}|l{1}|c{1}|t{1,2}|z|'.*?')/g),
+					orders = {
+						h: -1,
+						m: -1,
+						s: -1,
+						l: -1,
+						c: -1,
+						t: -1,
+						z: -1
+					};
+
+				if (finds) {
+					for (var i = 0; i < finds.length; i++) {
+						if (orders[finds[i].toString().charAt(0)] === -1) {
+							orders[finds[i].toString().charAt(0)] = i + 1;
+						}
+					}
+				}
+				return orders;
+			};
+
+			var regstr = '^' + f.toString()
+					.replace(/([hH]{1,2}|mm?|ss?|[tT]{1,2}|[zZ]|[lc]|'.*?')/g, function (match) {
+							var ml = match.length;
+							switch (match.charAt(0).toLowerCase()) {
+							case 'h':
+								return ml === 1 ? '(\\d?\\d)' : '(\\d{' + ml + '})';
+							case 'm':
+								return ml === 1 ? '(\\d?\\d)' : '(\\d{' + ml + '})';
+							case 's':
+								return ml === 1 ? '(\\d?\\d)' : '(\\d{' + ml + '})';
+							case 'l':
+								return '(\\d?\\d?\\d)';
+							case 'c':
+								return '(\\d?\\d?\\d)';
+							case 'z':
+								return '(z|[-+]\\d\\d:?\\d\\d|\\S+)?';
+							case 't':
+								return getPatternAmpm(o.amNames, o.pmNames);
+							default:    // literal escaped in quotes
+								return '(' + match.replace(/\'/g, "").replace(/(\.|\$|\^|\\|\/|\(|\)|\[|\]|\?|\+|\*)/g, function (m) { return "\\" + m; }) + ')?';
+							}
+						})
+					.replace(/\s/g, '\\s?') +
+					o.timeSuffix + '$',
+				order = getFormatPositions(f),
+				ampm = '',
+				treg;
+
+			treg = s.match(new RegExp(regstr, 'i'));
+
+			var resTime = {
+				hour: 0,
+				minute: 0,
+				second: 0,
+				millisec: 0,
+				microsec: 0
+			};
+
+			if (treg) {
+				if (order.t !== -1) {
+					if (treg[order.t] === undefined || treg[order.t].length === 0) {
+						ampm = '';
+						resTime.ampm = '';
+					} else {
+						ampm = $.inArray(treg[order.t].toUpperCase(), o.amNames) !== -1 ? 'AM' : 'PM';
+						resTime.ampm = o[ampm === 'AM' ? 'amNames' : 'pmNames'][0];
+					}
+				}
+
+				if (order.h !== -1) {
+					if (ampm === 'AM' && treg[order.h] === '12') {
+						resTime.hour = 0; // 12am = 0 hour
+					} else {
+						if (ampm === 'PM' && treg[order.h] !== '12') {
+							resTime.hour = parseInt(treg[order.h], 10) + 12; // 12pm = 12 hour, any other pm = hour + 12
+						} else {
+							resTime.hour = Number(treg[order.h]);
+						}
+					}
+				}
+
+				if (order.m !== -1) {
+					resTime.minute = Number(treg[order.m]);
+				}
+				if (order.s !== -1) {
+					resTime.second = Number(treg[order.s]);
+				}
+				if (order.l !== -1) {
+					resTime.millisec = Number(treg[order.l]);
+				}
+				if (order.c !== -1) {
+					resTime.microsec = Number(treg[order.c]);
+				}
+				if (order.z !== -1 && treg[order.z] !== undefined) {
+					resTime.timezone = $.timepicker.timezoneOffsetNumber(treg[order.z]);
+				}
+
+
+				return resTime;
+			}
+			return false;
+		};// end strictParse
+
+		// First try JS Date, if that fails, use strictParse
+		var looseParse = function (f, s, o) {
+			try {
+				var d = new Date('2012-01-01 ' + s);
+				if (isNaN(d.getTime())) {
+					d = new Date('2012-01-01T' + s);
+					if (isNaN(d.getTime())) {
+						d = new Date('01/01/2012 ' + s);
+						if (isNaN(d.getTime())) {
+							throw "Unable to parse time with native Date: " + s;
+						}
+					}
+				}
+
+				return {
+					hour: d.getHours(),
+					minute: d.getMinutes(),
+					second: d.getSeconds(),
+					millisec: d.getMilliseconds(),
+					microsec: d.getMicroseconds(),
+					timezone: d.getTimezoneOffset() * -1
+				};
+			}
+			catch (err) {
+				try {
+					return strictParse(f, s, o);
+				}
+				catch (err2) {
+					$.timepicker.log("Unable to parse \ntimeString: " + s + "\ntimeFormat: " + f);
+				}				
+			}
+			return false;
+		}; // end looseParse
+		
+		if (typeof o.parse === "function") {
+			return o.parse(timeFormat, timeString, o);
+		}
+		if (o.parse === 'loose') {
+			return looseParse(timeFormat, timeString, o);
+		}
+		return strictParse(timeFormat, timeString, o);
+	};
+
+	/**
+	 * Public utility to format the time
+	 * @param {string} format format of the time
+	 * @param {Object} time Object not a Date for timezones
+	 * @param {Object} [options] essentially the regional[].. amNames, pmNames, ampm
+	 * @returns {string} the formatted time
+	 */
+	$.datepicker.formatTime = function (format, time, options) {
+		options = options || {};
+		options = $.extend({}, $.timepicker._defaults, options);
+		time = $.extend({
+			hour: 0,
+			minute: 0,
+			second: 0,
+			millisec: 0,
+			microsec: 0,
+			timezone: null
+		}, time);
+
+		var tmptime = format,
+			ampmName = options.amNames[0],
+			hour = parseInt(time.hour, 10);
+
+		if (hour > 11) {
+			ampmName = options.pmNames[0];
+		}
+
+		tmptime = tmptime.replace(/(?:HH?|hh?|mm?|ss?|[tT]{1,2}|[zZ]|[lc]|'.*?')/g, function (match) {
+			switch (match) {
+			case 'HH':
+				return ('0' + hour).slice(-2);
+			case 'H':
+				return hour;
+			case 'hh':
+				return ('0' + convert24to12(hour)).slice(-2);
+			case 'h':
+				return convert24to12(hour);
+			case 'mm':
+				return ('0' + time.minute).slice(-2);
+			case 'm':
+				return time.minute;
+			case 'ss':
+				return ('0' + time.second).slice(-2);
+			case 's':
+				return time.second;
+			case 'l':
+				return ('00' + time.millisec).slice(-3);
+			case 'c':
+				return ('00' + time.microsec).slice(-3);
+			case 'z':
+				return $.timepicker.timezoneOffsetString(time.timezone === null ? options.timezone : time.timezone, false);
+			case 'Z':
+				return $.timepicker.timezoneOffsetString(time.timezone === null ? options.timezone : time.timezone, true);
+			case 'T':
+				return ampmName.charAt(0).toUpperCase();
+			case 'TT':
+				return ampmName.toUpperCase();
+			case 't':
+				return ampmName.charAt(0).toLowerCase();
+			case 'tt':
+				return ampmName.toLowerCase();
+			default:
+				return match.replace(/'/g, "");
+			}
+		});
+
+		return tmptime;
+	};
+
+	/*
+	* the bad hack :/ override datepicker so it doesn't close on select
+	// inspired: http://stackoverflow.com/questions/1252512/jquery-datepicker-prevent-closing-picker-when-clicking-a-date/1762378#1762378
+	*/
+	$.datepicker._base_selectDate = $.datepicker._selectDate;
+	$.datepicker._selectDate = function (id, dateStr) {
+		var inst = this._getInst($(id)[0]),
+			tp_inst = this._get(inst, 'timepicker');
+
+		if (tp_inst) {
+			tp_inst._limitMinMaxDateTime(inst, true);
+			inst.inline = inst.stay_open = true;
+			//This way the onSelect handler called from calendarpicker get the full dateTime
+			this._base_selectDate(id, dateStr);
+			inst.inline = inst.stay_open = false;
+			this._notifyChange(inst);
+			this._updateDatepicker(inst);
+		} else {
+			this._base_selectDate(id, dateStr);
+		}
+	};
+
+	/*
+	* second bad hack :/ override datepicker so it triggers an event when changing the input field
+	* and does not redraw the datepicker on every selectDate event
+	*/
+	$.datepicker._base_updateDatepicker = $.datepicker._updateDatepicker;
+	$.datepicker._updateDatepicker = function (inst) {
+
+		// don't popup the datepicker if there is another instance already opened
+		var input = inst.input[0];
+		if ($.datepicker._curInst && $.datepicker._curInst !== inst && $.datepicker._datepickerShowing && $.datepicker._lastInput !== input) {
+			return;
+		}
+
+		if (typeof(inst.stay_open) !== 'boolean' || inst.stay_open === false) {
+
+			this._base_updateDatepicker(inst);
+
+			// Reload the time control when changing something in the input text field.
+			var tp_inst = this._get(inst, 'timepicker');
+			if (tp_inst) {
+				tp_inst._addTimePicker(inst);
+			}
+		}
+	};
+
+	/*
+	* third bad hack :/ override datepicker so it allows spaces and colon in the input field
+	*/
+	$.datepicker._base_doKeyPress = $.datepicker._doKeyPress;
+	$.datepicker._doKeyPress = function (event) {
+		var inst = $.datepicker._getInst(event.target),
+			tp_inst = $.datepicker._get(inst, 'timepicker');
+
+		if (tp_inst) {
+			if ($.datepicker._get(inst, 'constrainInput')) {
+				var ampm = tp_inst.support.ampm,
+					tz = tp_inst._defaults.showTimezone !== null ? tp_inst._defaults.showTimezone : tp_inst.support.timezone,
+					dateChars = $.datepicker._possibleChars($.datepicker._get(inst, 'dateFormat')),
+					datetimeChars = tp_inst._defaults.timeFormat.toString()
+											.replace(/[hms]/g, '')
+											.replace(/TT/g, ampm ? 'APM' : '')
+											.replace(/Tt/g, ampm ? 'AaPpMm' : '')
+											.replace(/tT/g, ampm ? 'AaPpMm' : '')
+											.replace(/T/g, ampm ? 'AP' : '')
+											.replace(/tt/g, ampm ? 'apm' : '')
+											.replace(/t/g, ampm ? 'ap' : '') + 
+											" " + tp_inst._defaults.separator + 
+											tp_inst._defaults.timeSuffix + 
+											(tz ? tp_inst._defaults.timezoneList.join('') : '') + 
+											(tp_inst._defaults.amNames.join('')) + (tp_inst._defaults.pmNames.join('')) + 
+											dateChars,
+					chr = String.fromCharCode(event.charCode === undefined ? event.keyCode : event.charCode);
+				return event.ctrlKey || (chr < ' ' || !dateChars || datetimeChars.indexOf(chr) > -1);
+			}
+		}
+
+		return $.datepicker._base_doKeyPress(event);
+	};
+
+	/*
+	* Fourth bad hack :/ override _updateAlternate function used in inline mode to init altField
+	* Update any alternate field to synchronise with the main field.
+	*/
+	$.datepicker._base_updateAlternate = $.datepicker._updateAlternate;
+	$.datepicker._updateAlternate = function (inst) {
+		var tp_inst = this._get(inst, 'timepicker');
+		if (tp_inst) {
+			var altField = tp_inst._defaults.altField;
+			if (altField) { // update alternate field too
+				var altFormat = tp_inst._defaults.altFormat || tp_inst._defaults.dateFormat,
+					date = this._getDate(inst),
+					formatCfg = $.datepicker._getFormatConfig(inst),
+					altFormattedDateTime = '', 
+					altSeparator = tp_inst._defaults.altSeparator ? tp_inst._defaults.altSeparator : tp_inst._defaults.separator, 
+					altTimeSuffix = tp_inst._defaults.altTimeSuffix ? tp_inst._defaults.altTimeSuffix : tp_inst._defaults.timeSuffix,
+					altTimeFormat = tp_inst._defaults.altTimeFormat !== null ? tp_inst._defaults.altTimeFormat : tp_inst._defaults.timeFormat;
+				
+				altFormattedDateTime += $.datepicker.formatTime(altTimeFormat, tp_inst, tp_inst._defaults) + altTimeSuffix;
+				if (!tp_inst._defaults.timeOnly && !tp_inst._defaults.altFieldTimeOnly && date !== null) {
+					if (tp_inst._defaults.altFormat) {
+						altFormattedDateTime = $.datepicker.formatDate(tp_inst._defaults.altFormat, date, formatCfg) + altSeparator + altFormattedDateTime;
+					}
+					else {
+						altFormattedDateTime = tp_inst.formattedDate + altSeparator + altFormattedDateTime;
+					}
+				}
+				$(altField).val(altFormattedDateTime);
+			}
+		}
+		else {
+			$.datepicker._base_updateAlternate(inst);
+		}
+	};
+
+	/*
+	* Override key up event to sync manual input changes.
+	*/
+	$.datepicker._base_doKeyUp = $.datepicker._doKeyUp;
+	$.datepicker._doKeyUp = function (event) {
+		var inst = $.datepicker._getInst(event.target),
+			tp_inst = $.datepicker._get(inst, 'timepicker');
+
+		if (tp_inst) {
+			if (tp_inst._defaults.timeOnly && (inst.input.val() !== inst.lastVal)) {
+				try {
+					$.datepicker._updateDatepicker(inst);
+				} catch (err) {
+					$.timepicker.log(err);
+				}
+			}
+		}
+
+		return $.datepicker._base_doKeyUp(event);
+	};
+
+	/*
+	* override "Today" button to also grab the time.
+	*/
+	$.datepicker._base_gotoToday = $.datepicker._gotoToday;
+	$.datepicker._gotoToday = function (id) {
+		var inst = this._getInst($(id)[0]),
+			$dp = inst.dpDiv;
+		this._base_gotoToday(id);
+		var tp_inst = this._get(inst, 'timepicker');
+		selectLocalTimezone(tp_inst);
+		var now = new Date();
+		this._setTime(inst, now);
+		$('.ui-datepicker-today', $dp).click();
+	};
+
+	/*
+	* Disable & enable the Time in the datetimepicker
+	*/
+	$.datepicker._disableTimepickerDatepicker = function (target) {
+		var inst = this._getInst(target);
+		if (!inst) {
+			return;
+		}
+
+		var tp_inst = this._get(inst, 'timepicker');
+		$(target).datepicker('getDate'); // Init selected[Year|Month|Day]
+		if (tp_inst) {
+			inst.settings.showTimepicker = false;
+			tp_inst._defaults.showTimepicker = false;
+			tp_inst._updateDateTime(inst);
+		}
+	};
+
+	$.datepicker._enableTimepickerDatepicker = function (target) {
+		var inst = this._getInst(target);
+		if (!inst) {
+			return;
+		}
+
+		var tp_inst = this._get(inst, 'timepicker');
+		$(target).datepicker('getDate'); // Init selected[Year|Month|Day]
+		if (tp_inst) {
+			inst.settings.showTimepicker = true;
+			tp_inst._defaults.showTimepicker = true;
+			tp_inst._addTimePicker(inst); // Could be disabled on page load
+			tp_inst._updateDateTime(inst);
+		}
+	};
+
+	/*
+	* Create our own set time function
+	*/
+	$.datepicker._setTime = function (inst, date) {
+		var tp_inst = this._get(inst, 'timepicker');
+		if (tp_inst) {
+			var defaults = tp_inst._defaults;
+
+			// calling _setTime with no date sets time to defaults
+			tp_inst.hour = date ? date.getHours() : defaults.hour;
+			tp_inst.minute = date ? date.getMinutes() : defaults.minute;
+			tp_inst.second = date ? date.getSeconds() : defaults.second;
+			tp_inst.millisec = date ? date.getMilliseconds() : defaults.millisec;
+			tp_inst.microsec = date ? date.getMicroseconds() : defaults.microsec;
+
+			//check if within min/max times.. 
+			tp_inst._limitMinMaxDateTime(inst, true);
+
+			tp_inst._onTimeChange();
+			tp_inst._updateDateTime(inst);
+		}
+	};
+
+	/*
+	* Create new public method to set only time, callable as $().datepicker('setTime', date)
+	*/
+	$.datepicker._setTimeDatepicker = function (target, date, withDate) {
+		var inst = this._getInst(target);
+		if (!inst) {
+			return;
+		}
+
+		var tp_inst = this._get(inst, 'timepicker');
+
+		if (tp_inst) {
+			this._setDateFromField(inst);
+			var tp_date;
+			if (date) {
+				if (typeof date === "string") {
+					tp_inst._parseTime(date, withDate);
+					tp_date = new Date();
+					tp_date.setHours(tp_inst.hour, tp_inst.minute, tp_inst.second, tp_inst.millisec);
+					tp_date.setMicroseconds(tp_inst.microsec);
+				} else {
+					tp_date = new Date(date.getTime());
+					tp_date.setMicroseconds(date.getMicroseconds());
+				}
+				if (tp_date.toString() === 'Invalid Date') {
+					tp_date = undefined;
+				}
+				this._setTime(inst, tp_date);
+			}
+		}
+
+	};
+
+	/*
+	* override setDate() to allow setting time too within Date object
+	*/
+	$.datepicker._base_setDateDatepicker = $.datepicker._setDateDatepicker;
+	$.datepicker._setDateDatepicker = function (target, date) {
+		var inst = this._getInst(target);
+		if (!inst) {
+			return;
+		}
+
+		if (typeof(date) === 'string') {
+			date = new Date(date);
+			if (!date.getTime()) {
+				$.timepicker.log("Error creating Date object from string.");
+			}
+		}
+
+		var tp_inst = this._get(inst, 'timepicker');
+		var tp_date;
+		if (date instanceof Date) {
+			tp_date = new Date(date.getTime());
+			tp_date.setMicroseconds(date.getMicroseconds());
+		} else {
+			tp_date = date;
+		}
+		
+		// This is important if you are using the timezone option, javascript's Date 
+		// object will only return the timezone offset for the current locale, so we 
+		// adjust it accordingly.  If not using timezone option this won't matter..
+		// If a timezone is different in tp, keep the timezone as is
+		if (tp_inst && tp_date) {
+			// look out for DST if tz wasn't specified
+			if (!tp_inst.support.timezone && tp_inst._defaults.timezone === null) {
+				tp_inst.timezone = tp_date.getTimezoneOffset() * -1;
+			}
+			date = $.timepicker.timezoneAdjust(date, tp_inst.timezone);
+			tp_date = $.timepicker.timezoneAdjust(tp_date, tp_inst.timezone);
+		}
+
+		this._updateDatepicker(inst);
+		this._base_setDateDatepicker.apply(this, arguments);
+		this._setTimeDatepicker(target, tp_date, true);
+	};
+
+	/*
+	* override getDate() to allow getting time too within Date object
+	*/
+	$.datepicker._base_getDateDatepicker = $.datepicker._getDateDatepicker;
+	$.datepicker._getDateDatepicker = function (target, noDefault) {
+		var inst = this._getInst(target);
+		if (!inst) {
+			return;
+		}
+
+		var tp_inst = this._get(inst, 'timepicker');
+
+		if (tp_inst) {
+			// if it hasn't yet been defined, grab from field
+			if (inst.lastVal === undefined) {
+				this._setDateFromField(inst, noDefault);
+			}
+
+			var date = this._getDate(inst);
+			if (date && tp_inst._parseTime($(target).val(), tp_inst.timeOnly)) {
+				date.setHours(tp_inst.hour, tp_inst.minute, tp_inst.second, tp_inst.millisec);
+				date.setMicroseconds(tp_inst.microsec);
+
+				// This is important if you are using the timezone option, javascript's Date 
+				// object will only return the timezone offset for the current locale, so we 
+				// adjust it accordingly.  If not using timezone option this won't matter..
+				if (tp_inst.timezone != null) {
+					// look out for DST if tz wasn't specified
+					if (!tp_inst.support.timezone && tp_inst._defaults.timezone === null) {
+						tp_inst.timezone = date.getTimezoneOffset() * -1;
+					}
+					date = $.timepicker.timezoneAdjust(date, tp_inst.timezone);
+				}
+			}
+			return date;
+		}
+		return this._base_getDateDatepicker(target, noDefault);
+	};
+
+	/*
+	* override parseDate() because UI 1.8.14 throws an error about "Extra characters"
+	* An option in datapicker to ignore extra format characters would be nicer.
+	*/
+	$.datepicker._base_parseDate = $.datepicker.parseDate;
+	$.datepicker.parseDate = function (format, value, settings) {
+		var date;
+		try {
+			date = this._base_parseDate(format, value, settings);
+		} catch (err) {
+			// Hack!  The error message ends with a colon, a space, and
+			// the "extra" characters.  We rely on that instead of
+			// attempting to perfectly reproduce the parsing algorithm.
+			if (err.indexOf(":") >= 0) {
+				date = this._base_parseDate(format, value.substring(0, value.length - (err.length - err.indexOf(':') - 2)), settings);
+				$.timepicker.log("Error parsing the date string: " + err + "\ndate string = " + value + "\ndate format = " + format);
+			} else {
+				throw err;
+			}
+		}
+		return date;
+	};
+
+	/*
+	* override formatDate to set date with time to the input
+	*/
+	$.datepicker._base_formatDate = $.datepicker._formatDate;
+	$.datepicker._formatDate = function (inst, day, month, year) {
+		var tp_inst = this._get(inst, 'timepicker');
+		if (tp_inst) {
+			tp_inst._updateDateTime(inst);
+			return tp_inst.$input.val();
+		}
+		return this._base_formatDate(inst);
+	};
+
+	/*
+	* override options setter to add time to maxDate(Time) and minDate(Time). MaxDate
+	*/
+	$.datepicker._base_optionDatepicker = $.datepicker._optionDatepicker;
+	$.datepicker._optionDatepicker = function (target, name, value) {
+		var inst = this._getInst(target),
+			name_clone;
+		if (!inst) {
+			return null;
+		}
+
+		var tp_inst = this._get(inst, 'timepicker');
+		if (tp_inst) {
+			var min = null,
+				max = null,
+				onselect = null,
+				overrides = tp_inst._defaults.evnts,
+				fns = {},
+				prop;
+			if (typeof name === 'string') { // if min/max was set with the string
+				if (name === 'minDate' || name === 'minDateTime') {
+					min = value;
+				} else if (name === 'maxDate' || name === 'maxDateTime') {
+					max = value;
+				} else if (name === 'onSelect') {
+					onselect = value;
+				} else if (overrides.hasOwnProperty(name)) {
+					if (typeof (value) === 'undefined') {
+						return overrides[name];
+					}
+					fns[name] = value;
+					name_clone = {}; //empty results in exiting function after overrides updated
+				}
+			} else if (typeof name === 'object') { //if min/max was set with the JSON
+				if (name.minDate) {
+					min = name.minDate;
+				} else if (name.minDateTime) {
+					min = name.minDateTime;
+				} else if (name.maxDate) {
+					max = name.maxDate;
+				} else if (name.maxDateTime) {
+					max = name.maxDateTime;
+				}
+				for (prop in overrides) {
+					if (overrides.hasOwnProperty(prop) && name[prop]) {
+						fns[prop] = name[prop];
+					}
+				}
+			}
+			for (prop in fns) {
+				if (fns.hasOwnProperty(prop)) {
+					overrides[prop] = fns[prop];
+					if (!name_clone) { name_clone = $.extend({}, name); }
+					delete name_clone[prop];
+				}
+			}
+			if (name_clone && isEmptyObject(name_clone)) { return; }
+			if (min) { //if min was set
+				if (min === 0) {
+					min = new Date();
+				} else {
+					min = new Date(min);
+				}
+				tp_inst._defaults.minDate = min;
+				tp_inst._defaults.minDateTime = min;
+			} else if (max) { //if max was set
+				if (max === 0) {
+					max = new Date();
+				} else {
+					max = new Date(max);
+				}
+				tp_inst._defaults.maxDate = max;
+				tp_inst._defaults.maxDateTime = max;
+			} else if (onselect) {
+				tp_inst._defaults.onSelect = onselect;
+			}
+		}
+		if (value === undefined) {
+			return this._base_optionDatepicker.call($.datepicker, target, name);
+		}
+		return this._base_optionDatepicker.call($.datepicker, target, name_clone || name, value);
+	};
+	
+	/*
+	* jQuery isEmptyObject does not check hasOwnProperty - if someone has added to the object prototype,
+	* it will return false for all objects
+	*/
+	var isEmptyObject = function (obj) {
+		var prop;
+		for (prop in obj) {
+			if (obj.hasOwnProperty(prop)) {
+				return false;
+			}
+		}
+		return true;
+	};
+
+	/*
+	* jQuery extend now ignores nulls!
+	*/
+	var extendRemove = function (target, props) {
+		$.extend(target, props);
+		for (var name in props) {
+			if (props[name] === null || props[name] === undefined) {
+				target[name] = props[name];
+			}
+		}
+		return target;
+	};
+
+	/*
+	* Determine by the time format which units are supported
+	* Returns an object of booleans for each unit
+	*/
+	var detectSupport = function (timeFormat) {
+		var tf = timeFormat.replace(/'.*?'/g, '').toLowerCase(), // removes literals
+			isIn = function (f, t) { // does the format contain the token?
+					return f.indexOf(t) !== -1 ? true : false;
+				};
+		return {
+				hour: isIn(tf, 'h'),
+				minute: isIn(tf, 'm'),
+				second: isIn(tf, 's'),
+				millisec: isIn(tf, 'l'),
+				microsec: isIn(tf, 'c'),
+				timezone: isIn(tf, 'z'),
+				ampm: isIn(tf, 't') && isIn(timeFormat, 'h'),
+				iso8601: isIn(timeFormat, 'Z')
+			};
+	};
+
+	/*
+	* Converts 24 hour format into 12 hour
+	* Returns 12 hour without leading 0
+	*/
+	var convert24to12 = function (hour) {
+		hour %= 12;
+
+		if (hour === 0) {
+			hour = 12;
+		}
+
+		return String(hour);
+	};
+
+	var computeEffectiveSetting = function (settings, property) {
+		return settings && settings[property] ? settings[property] : $.timepicker._defaults[property];
+	};
+
+	/*
+	* Splits datetime string into date and time substrings.
+	* Throws exception when date can't be parsed
+	* Returns {dateString: dateString, timeString: timeString}
+	*/
+	var splitDateTime = function (dateTimeString, timeSettings) {
+		// The idea is to get the number separator occurrences in datetime and the time format requested (since time has
+		// fewer unknowns, mostly numbers and am/pm). We will use the time pattern to split.
+		var separator = computeEffectiveSetting(timeSettings, 'separator'),
+			format = computeEffectiveSetting(timeSettings, 'timeFormat'),
+			timeParts = format.split(separator), // how many occurrences of separator may be in our format?
+			timePartsLen = timeParts.length,
+			allParts = dateTimeString.split(separator),
+			allPartsLen = allParts.length;
+
+		if (allPartsLen > 1) {
+			return {
+				dateString: allParts.splice(0, allPartsLen - timePartsLen).join(separator),
+				timeString: allParts.splice(0, timePartsLen).join(separator)
+			};
+		}
+
+		return {
+			dateString: dateTimeString,
+			timeString: ''
+		};
+	};
+
+	/*
+	* Internal function to parse datetime interval
+	* Returns: {date: Date, timeObj: Object}, where
+	*   date - parsed date without time (type Date)
+	*   timeObj = {hour: , minute: , second: , millisec: , microsec: } - parsed time. Optional
+	*/
+	var parseDateTimeInternal = function (dateFormat, timeFormat, dateTimeString, dateSettings, timeSettings) {
+		var date,
+			parts,
+			parsedTime;
+
+		parts = splitDateTime(dateTimeString, timeSettings);
+		date = $.datepicker._base_parseDate(dateFormat, parts.dateString, dateSettings);
+
+		if (parts.timeString === '') {
+			return {
+				date: date
+			};
+		}
+
+		parsedTime = $.datepicker.parseTime(timeFormat, parts.timeString, timeSettings);
+
+		if (!parsedTime) {
+			throw 'Wrong time format';
+		}
+
+		return {
+			date: date,
+			timeObj: parsedTime
+		};
+	};
+
+	/*
+	* Internal function to set timezone_select to the local timezone
+	*/
+	var selectLocalTimezone = function (tp_inst, date) {
+		if (tp_inst && tp_inst.timezone_select) {
+			var now = date || new Date();
+			tp_inst.timezone_select.val(-now.getTimezoneOffset());
+		}
+	};
+
+	/*
+	* Create a Singleton Instance
+	*/
+	$.timepicker = new Timepicker();
+
+	/**
+	 * Get the timezone offset as string from a date object (eg '+0530' for UTC+5.5)
+	 * @param {number} tzMinutes if not a number, less than -720 (-1200), or greater than 840 (+1400) this value is returned
+	 * @param {boolean} iso8601 if true formats in accordance to iso8601 "+12:45"
+	 * @return {string}
+	 */
+	$.timepicker.timezoneOffsetString = function (tzMinutes, iso8601) {
+		if (isNaN(tzMinutes) || tzMinutes > 840 || tzMinutes < -720) {
+			return tzMinutes;
+		}
+
+		var off = tzMinutes,
+			minutes = off % 60,
+			hours = (off - minutes) / 60,
+			iso = iso8601 ? ':' : '',
+			tz = (off >= 0 ? '+' : '-') + ('0' + Math.abs(hours)).slice(-2) + iso + ('0' + Math.abs(minutes)).slice(-2);
+		
+		if (tz === '+00:00') {
+			return 'Z';
+		}
+		return tz;
+	};
+
+	/**
+	 * Get the number in minutes that represents a timezone string
+	 * @param  {string} tzString formatted like "+0500", "-1245", "Z"
+	 * @return {number} the offset minutes or the original string if it doesn't match expectations
+	 */
+	$.timepicker.timezoneOffsetNumber = function (tzString) {
+		var normalized = tzString.toString().replace(':', ''); // excuse any iso8601, end up with "+1245"
+
+		if (normalized.toUpperCase() === 'Z') { // if iso8601 with Z, its 0 minute offset
+			return 0;
+		}
+
+		if (!/^(\-|\+)\d{4}$/.test(normalized)) { // possibly a user defined tz, so just give it back
+			return tzString;
+		}
+
+		return ((normalized.substr(0, 1) === '-' ? -1 : 1) * // plus or minus
+					((parseInt(normalized.substr(1, 2), 10) * 60) + // hours (converted to minutes)
+					parseInt(normalized.substr(3, 2), 10))); // minutes
+	};
+
+	/**
+	 * No way to set timezone in js Date, so we must adjust the minutes to compensate. (think setDate, getDate)
+	 * @param  {Date} date
+	 * @param  {string} toTimezone formatted like "+0500", "-1245"
+	 * @return {Date}
+	 */
+	$.timepicker.timezoneAdjust = function (date, toTimezone) {
+		var toTz = $.timepicker.timezoneOffsetNumber(toTimezone);
+		if (!isNaN(toTz)) {
+			date.setMinutes(date.getMinutes() + -date.getTimezoneOffset() - toTz);
+		}
+		return date;
+	};
+
+	/**
+	 * Calls `timepicker()` on the `startTime` and `endTime` elements, and configures them to
+	 * enforce date range limits.
+	 * n.b. The input value must be correctly formatted (reformatting is not supported)
+	 * @param  {Element} startTime
+	 * @param  {Element} endTime
+	 * @param  {Object} options Options for the timepicker() call
+	 * @return {jQuery}
+	 */
+	$.timepicker.timeRange = function (startTime, endTime, options) {
+		return $.timepicker.handleRange('timepicker', startTime, endTime, options);
+	};
+
+	/**
+	 * Calls `datetimepicker` on the `startTime` and `endTime` elements, and configures them to
+	 * enforce date range limits.
+	 * @param  {Element} startTime
+	 * @param  {Element} endTime
+	 * @param  {Object} options Options for the `timepicker()` call. Also supports `reformat`,
+	 *   a boolean value that can be used to reformat the input values to the `dateFormat`.
+	 * @param  {string} method Can be used to specify the type of picker to be added
+	 * @return {jQuery}
+	 */
+	$.timepicker.datetimeRange = function (startTime, endTime, options) {
+		$.timepicker.handleRange('datetimepicker', startTime, endTime, options);
+	};
+
+	/**
+	 * Calls `datepicker` on the `startTime` and `endTime` elements, and configures them to
+	 * enforce date range limits.
+	 * @param  {Element} startTime
+	 * @param  {Element} endTime
+	 * @param  {Object} options Options for the `timepicker()` call. Also supports `reformat`,
+	 *   a boolean value that can be used to reformat the input values to the `dateFormat`.
+	 * @return {jQuery}
+	 */
+	$.timepicker.dateRange = function (startTime, endTime, options) {
+		$.timepicker.handleRange('datepicker', startTime, endTime, options);
+	};
+
+	/**
+	 * Calls `method` on the `startTime` and `endTime` elements, and configures them to
+	 * enforce date range limits.
+	 * @param  {string} method Can be used to specify the type of picker to be added
+	 * @param  {Element} startTime
+	 * @param  {Element} endTime
+	 * @param  {Object} options Options for the `timepicker()` call. Also supports `reformat`,
+	 *   a boolean value that can be used to reformat the input values to the `dateFormat`.
+	 * @return {jQuery}
+	 */
+	$.timepicker.handleRange = function (method, startTime, endTime, options) {
+		options = $.extend({}, {
+			minInterval: 0, // min allowed interval in milliseconds
+			maxInterval: 0, // max allowed interval in milliseconds
+			start: {},      // options for start picker
+			end: {}         // options for end picker
+		}, options);
+
+		function checkDates(changed, other) {
+			var startdt = startTime[method]('getDate'),
+				enddt = endTime[method]('getDate'),
+				changeddt = changed[method]('getDate');
+
+			if (startdt !== null) {
+				var minDate = new Date(startdt.getTime()),
+					maxDate = new Date(startdt.getTime());
+
+				minDate.setMilliseconds(minDate.getMilliseconds() + options.minInterval);
+				maxDate.setMilliseconds(maxDate.getMilliseconds() + options.maxInterval);
+
+				if (options.minInterval > 0 && minDate > enddt) { // minInterval check
+					endTime[method]('setDate', minDate);
+				}
+				else if (options.maxInterval > 0 && maxDate < enddt) { // max interval check
+					endTime[method]('setDate', maxDate);
+				}
+				else if (startdt > enddt) {
+					other[method]('setDate', changeddt);
+				}
+			}
+		}
+
+		function selected(changed, other, option) {
+			if (!changed.val()) {
+				return;
+			}
+			var date = changed[method].call(changed, 'getDate');
+			if (date !== null && options.minInterval > 0) {
+				if (option === 'minDate') {
+					date.setMilliseconds(date.getMilliseconds() + options.minInterval);
+				}
+				if (option === 'maxDate') {
+					date.setMilliseconds(date.getMilliseconds() - options.minInterval);
+				}
+			}
+			if (date.getTime) {
+				other[method].call(other, 'option', option, date);
+			}
+		}
+
+		$.fn[method].call(startTime, $.extend({
+			onClose: function (dateText, inst) {
+				checkDates($(this), endTime);
+			},
+			onSelect: function (selectedDateTime) {
+				selected($(this), endTime, 'minDate');
+			}
+		}, options, options.start));
+		$.fn[method].call(endTime, $.extend({
+			onClose: function (dateText, inst) {
+				checkDates($(this), startTime);
+			},
+			onSelect: function (selectedDateTime) {
+				selected($(this), startTime, 'maxDate');
+			}
+		}, options, options.end));
+
+		checkDates(startTime, endTime);
+		selected(startTime, endTime, 'minDate');
+		selected(endTime, startTime, 'maxDate');
+		return $([startTime.get(0), endTime.get(0)]);
+	};
+
+	/**
+	 * Log error or data to the console during error or debugging
+	 * @param  {Object} err pass any type object to log to the console during error or debugging
+	 * @return {void}
+	 */
+	$.timepicker.log = function (err) {
+		if (window.console) {
+			window.console.log(err);
+		}
+	};
+
+	/*
+	 * Add util object to allow access to private methods for testability.
+	 */
+	$.timepicker._util = {
+		_extendRemove: extendRemove,
+		_isEmptyObject: isEmptyObject,
+		_convert24to12: convert24to12,
+		_detectSupport: detectSupport,
+		_selectLocalTimezone: selectLocalTimezone,
+		_computeEffectiveSetting: computeEffectiveSetting,
+		_splitDateTime: splitDateTime,
+		_parseDateTimeInternal: parseDateTimeInternal
+	};
+
+	/*
+	* Microsecond support
+	*/
+	if (!Date.prototype.getMicroseconds) {
+		Date.prototype.microseconds = 0;
+		Date.prototype.getMicroseconds = function () { return this.microseconds; };
+		Date.prototype.setMicroseconds = function (m) {
+			this.setMilliseconds(this.getMilliseconds() + Math.floor(m / 1000));
+			this.microseconds = m % 1000;
+			return this;
+		};
+	}
+
+	/*
+	* Keep up with the version
+	*/
+	$.timepicker.version = "1.4.3";
+
+})(jQuery);

+ 6 - 0
site/jquery.treegrid.css

@@ -0,0 +1,6 @@
+.treegrid-indent {width:16px; height: 16px; display: inline-block; position: relative;}
+
+.treegrid-expander {width:16px; height: 16px; display: inline-block; position: relative; cursor: pointer;}
+
+.treegrid-expander-expanded{background-image: url(img/collapse.png); }
+.treegrid-expander-collapsed{background-image: url(img/expand.png);}

+ 619 - 0
site/jquery.treegrid.js

@@ -0,0 +1,619 @@
+/*
+ * jQuery treegrid Plugin 0.2.0
+ * https://github.com/maxazan/jquery-treegrid
+ * 
+ * Copyright 2013, Pomazan Max
+ * Licensed under the MIT licenses.
+ */
+(function($) {
+
+    var methods = {
+        /**
+         * Initialize tree
+         * 
+         * @param {Object} options
+         * @returns {Object[]}
+         */
+        initTree: function(options) {
+            var settings = $.extend({}, this.treegrid.defaults, options);
+            return this.each(function() {
+                var $this = $(this);
+                $this.treegrid('setTreeContainer', $(this));
+                $this.treegrid('setSettings', settings);
+                settings.getRootNodes.apply(this, [$(this)]).treegrid('initNode', settings);
+            });
+        },
+        /**
+         * Initialize node
+         * 
+         * @param {Object} settings
+         * @returns {Object[]}
+         */
+        initNode: function(settings) {
+            return this.each(function() {
+                var $this = $(this);
+                $this.treegrid('setTreeContainer', settings.getTreeGridContainer.apply(this));
+                $this.treegrid('getChildNodes').treegrid('initNode', settings);
+                $this.treegrid('initExpander').treegrid('initIndent').treegrid('initEvents').treegrid('initState').treegrid("initSettingsEvents");
+            });
+        },
+        /**
+         * Initialize node events
+         * 
+         * @returns {Node}
+         */
+        initEvents: function() {
+            var $this = $(this);
+            //Save state on change
+            $this.on("change", function() {
+                var $this = $(this);
+                $this.treegrid('render');
+                if ($this.treegrid('getSetting', 'saveState')) {
+                    $this.treegrid('saveState');
+                }
+            });
+            //Default behavior on collapse
+            $this.on("collapse", function() {
+                var $this = $(this);
+                $this.removeClass('treegrid-expanded');
+                $this.addClass('treegrid-collapsed');
+            });
+            //Default behavior on expand
+            $this.on("expand", function() {
+                var $this = $(this);
+                $this.removeClass('treegrid-collapsed');
+                $this.addClass('treegrid-expanded');
+            });
+
+            return $this;
+        },
+        /**
+         * Initialize events from settings
+         * 
+         * @returns {Node}
+         */
+        initSettingsEvents: function() {
+            var $this = $(this);
+            //Save state on change
+            $this.on("change", function() {
+                var $this = $(this);
+                if (typeof ($this.treegrid('getSetting', 'onChange')) === "function") {
+                    $this.treegrid('getSetting', 'onChange').apply($this);
+                }
+            });
+            //Default behavior on collapse
+            $this.on("collapse", function() {
+                var $this = $(this);
+                if (typeof ($this.treegrid('getSetting', 'onCollapse')) === "function") {
+                    $this.treegrid('getSetting', 'onCollapse').apply($this);
+                }
+            });
+            //Default behavior on expand
+            $this.on("expand", function() {
+                var $this = $(this);
+                if (typeof ($this.treegrid('getSetting', 'onExpand')) === "function") {
+                    $this.treegrid('getSetting', 'onExpand').apply($this);
+                }
+
+            });
+
+            return $this;
+        },
+        /**
+         * Initialize expander for node
+         * 
+         * @returns {Node}
+         */
+        initExpander: function() {
+            var $this = $(this);
+            var cell = $this.find('td').get($this.treegrid('getSetting', 'treeColumn'));
+            var tpl = $this.treegrid('getSetting', 'expanderTemplate');
+            var expander = $this.treegrid('getSetting', 'getExpander').apply(this);
+            if (expander) {
+                expander.remove();
+            }
+            $(tpl).prependTo(cell).click(function() {
+                $($(this).closest('tr')).treegrid('toggle');
+            });
+            return $this;
+        },
+        /**
+         * Initialize indent for node
+         * 
+         * @returns {Node}
+         */
+        initIndent: function() {
+            var $this = $(this);
+            $this.find('.treegrid-indent').remove();
+            for (var i = 0; i < $(this).treegrid('getDepth'); i++) {
+                $($this.treegrid('getSetting', 'indentTemplate')).insertBefore($this.find('.treegrid-expander'));
+            }
+            return $this;
+        },
+        /**
+         * Initialise state of node
+         * 
+         * @returns {Node}
+         */
+        initState: function() {
+            var $this = $(this);
+            if ($this.treegrid('getSetting', 'saveState') && !$this.treegrid('isFirstInit')) {
+                $this.treegrid('restoreState');
+            } else {
+                if ($this.treegrid('getSetting', 'initialState') === "expanded") {
+                    $this.treegrid('expand');
+                } else {
+                    $this.treegrid('collapse');
+                }
+            }
+            return $this;
+        },
+        /**
+         * Return true if this tree was never been initialised
+         * 
+         * @returns {Boolean}
+         */
+        isFirstInit: function() {
+            var tree = $(this).treegrid('getTreeContainer');
+            if (tree.data('first_init') === undefined) {
+                tree.data('first_init', $.cookie(tree.treegrid('getSetting', 'saveStateName')) === undefined);
+            }
+            return tree.data('first_init');
+        },
+        /**
+         * Save state of current node
+         * 
+         * @returns {Node}
+         */
+        saveState: function() {
+            var $this = $(this);
+            if ($this.treegrid('getSetting', 'saveStateMethod') === 'cookie') {
+
+                var stateArrayString = $.cookie($this.treegrid('getSetting', 'saveStateName')) || '';
+                var stateArray = (stateArrayString === '' ? [] : stateArrayString.split(','));
+                var nodeId = $this.treegrid('getNodeId');
+
+                if ($this.treegrid('isExpanded')) {
+                    if ($.inArray(nodeId, stateArray) === -1) {
+                        stateArray.push(nodeId);
+                    }
+                } else if ($this.treegrid('isCollapsed')) {
+                    if ($.inArray(nodeId, stateArray) !== -1) {
+                        stateArray.splice($.inArray(nodeId, stateArray), 1);
+                    }
+                }
+                $.cookie($this.treegrid('getSetting', 'saveStateName'), stateArray.join(','));
+            }
+            return $this;
+        },
+        /**
+         * Restore state of current node.
+         * 
+         * @returns {Node}
+         */
+        restoreState: function() {
+            var $this = $(this);
+            if ($this.treegrid('getSetting', 'saveStateMethod') === 'cookie') {
+                var stateArray = $.cookie($this.treegrid('getSetting', 'saveStateName')).split(',');
+                if ($.inArray($this.treegrid('getNodeId'), stateArray) !== -1) {
+                    $this.treegrid('expand');
+                } else {
+                    $this.treegrid('collapse');
+                }
+
+            }
+            return $this;
+        },
+        /**
+         * Method return setting by name
+         * 
+         * @param {type} name
+         * @returns {unresolved}
+         */
+        getSetting: function(name) {
+            if (!$(this).treegrid('getTreeContainer')) {
+                return null;
+            }
+            return $(this).treegrid('getTreeContainer').data('settings')[name];
+        },
+        /**
+         * Add new settings
+         * 
+         * @param {Object} settings
+         */
+        setSettings: function(settings) {
+            $(this).treegrid('getTreeContainer').data('settings', settings);
+        },
+        /**
+         * Return tree container
+         * 
+         * @returns {HtmlElement}
+         */
+        getTreeContainer: function() {
+            return $(this).data('treegrid');
+        },
+        /**
+         * Set tree container
+         * 
+         * @param {HtmlE;ement} container
+         */
+        setTreeContainer: function(container) {
+            return $(this).data('treegrid', container);
+        },
+        /**
+         * Method return all root nodes of tree. 
+         * 
+         * Start init all child nodes from it.
+         * 
+         * @returns {Array}
+         */
+        getRootNodes: function() {
+            return $(this).treegrid('getSetting', 'getRootNodes').apply(this, [$(this).treegrid('getTreeContainer')]);
+        },
+        /**
+         * Method return all nodes of tree. 
+         * 
+         * @returns {Array}
+         */
+        getAllNodes: function() {
+            return $(this).treegrid('getSetting', 'getAllNodes').apply(this, [$(this).treegrid('getTreeContainer')]);
+        },
+        /**
+         * Mthod return true if element is Node
+         * 
+         * @returns {String}
+         */
+        isNode: function() {
+            return $(this).treegrid('getNodeId') !== null;
+        },
+        /**
+         * Mthod return id of node
+         * 
+         * @returns {String}
+         */
+        getNodeId: function() {
+            if ($(this).treegrid('getSetting', 'getNodeId') === null) {
+                return null;
+            } else {
+                return $(this).treegrid('getSetting', 'getNodeId').apply(this);
+            }
+        },
+        /**
+         * Method return parent id of node or null if root node
+         * 
+         * @returns {String}
+         */
+        getParentNodeId: function() {
+            return $(this).treegrid('getSetting', 'getParentNodeId').apply(this);
+        },
+        /**
+         * Method return parent node or null if root node
+         * 
+         * @returns {Object[]}
+         */
+        getParentNode: function() {
+            if ($(this).treegrid('getParentNodeId') === null) {
+                return null;
+            } else {
+                return $(this).treegrid('getSetting', 'getNodeById').apply(this, [$(this).treegrid('getParentNodeId'), $(this).treegrid('getTreeContainer')]);
+            }
+        },
+        /**
+         * Method return array of child nodes or null if node is leaf
+         * 
+         * @returns {Object[]}
+         */
+        getChildNodes: function() {
+            return $(this).treegrid('getSetting', 'getChildNodes').apply(this, [$(this).treegrid('getNodeId'), $(this).treegrid('getTreeContainer')]);
+        },
+        /**
+         * Method return depth of tree.
+         * 
+         * This method is needs for calculate indent
+         * 
+         * @returns {Number}
+         */
+        getDepth: function() {
+            if ($(this).treegrid('getParentNode') === null) {
+                return 0;
+            }
+            return $(this).treegrid('getParentNode').treegrid('getDepth') + 1;
+        },
+        /**
+         * Method return true if node is root
+         * 
+         * @returns {Boolean}
+         */
+        isRoot: function() {
+            return $(this).treegrid('getDepth') === 0;
+        },
+        /**
+         * Method return true if node has no child nodes
+         * 
+         * @returns {Boolean}
+         */
+        isLeaf: function() {
+            return $(this).treegrid('getChildNodes').length === 0;
+        },
+        /**
+         * Method return true if node last in branch
+         * 
+         * @returns {Boolean}
+         */
+        isLast: function() {
+            if ($(this).treegrid('isNode')) {
+                var parentNode = $(this).treegrid('getParentNode');
+                if (parentNode === null) {
+                    if ($(this).treegrid('getNodeId') === $(this).treegrid('getRootNodes').last().treegrid('getNodeId')) {
+                        return true;
+                    }
+                } else {
+                    if ($(this).treegrid('getNodeId') === parentNode.treegrid('getChildNodes').last().treegrid('getNodeId')) {
+                        return true;
+                    }
+                }
+            }
+            return false;
+        },
+        /**
+         * Method return true if node first in branch
+         * 
+         * @returns {Boolean}
+         */
+        isFirst: function() {
+            if ($(this).treegrid('isNode')) {
+                var parentNode = $(this).treegrid('getParentNode');
+                if (parentNode === null) {
+                    if ($(this).treegrid('getNodeId') === $(this).treegrid('getRootNodes').first().treegrid('getNodeId')) {
+                        return true;
+                    }
+                } else {
+                    if ($(this).treegrid('getNodeId') === parentNode.treegrid('getChildNodes').first().treegrid('getNodeId')) {
+                        return true;
+                    }
+                }
+            }
+            return false;
+        },
+        /**
+         * Return true if node expanded
+         * 
+         * @returns {Boolean}
+         */
+        isExpanded: function() {
+            return $(this).hasClass('treegrid-expanded');
+        },
+        /**
+         * Return true if node collapsed
+         * 
+         * @returns {Boolean}
+         */
+        isCollapsed: function() {
+            return $(this).hasClass('treegrid-collapsed');
+        },
+        /**
+         * Return true if at least one of parent node is collapsed
+         * 
+         * @returns {Boolean}
+         */
+        isOneOfParentsCollapsed: function() {
+            var $this = $(this);
+            if ($this.treegrid('isRoot')) {
+                return false;
+            } else {
+                if ($this.treegrid('getParentNode').treegrid('isCollapsed')) {
+                    return true;
+                } else {
+                    return $this.treegrid('getParentNode').treegrid('isOneOfParentsCollapsed');
+                }
+            }
+        },
+        /**
+         * Expand node
+         * 
+         * @returns {Node}
+         */
+        expand: function() {
+            return $(this).each(function() {
+                var $this = $(this);
+                if (!$this.treegrid('isLeaf') && !$this.treegrid("isExpanded")) {
+                    $this.trigger("expand");
+                    $this.trigger("change");
+                }
+            });
+        },
+        /**
+         * Expand all nodes
+         * 
+         * @returns {Node}
+         */
+        expandAll: function() {
+            var $this = $(this);
+            $this.treegrid('getRootNodes').treegrid('expandRecursive');
+            return $this;
+        },
+        /**
+         * Expand current node and all child nodes begin from current
+         * 
+         * @returns {Node}
+         */
+        expandRecursive: function() {
+            return $(this).each(function() {
+                var $this = $(this);
+                $this.treegrid('expand');
+                if (!$this.treegrid('isLeaf')) {
+                    $this.treegrid('getChildNodes').treegrid('expandRecursive');
+                }
+            });
+        },
+        /**
+         * Collapse node
+         * 
+         * @returns {Node}
+         */
+        collapse: function() {
+            return $(this).each(function() {
+                var $this = $(this);
+                if (!$this.treegrid('isLeaf') && !$this.treegrid("isCollapsed")) {
+                    $this.trigger("collapse");
+                    $this.trigger("change");
+                }
+            });
+        },
+        /**
+         * Collapse all nodes
+         * 
+         * @returns {Node}
+         */
+        collapseAll: function() {
+            var $this = $(this);
+            $this.treegrid('getRootNodes').treegrid('collapseRecursive');
+            return $this;
+        },
+        /**
+         * Collapse current node and all child nodes begin from current
+         * 
+         * @returns {Node}
+         */
+        collapseRecursive: function() {
+            return $(this).each(function() {
+                var $this = $(this);
+                $this.treegrid('collapse');
+                if (!$this.treegrid('isLeaf')) {
+                    $this.treegrid('getChildNodes').treegrid('collapseRecursive');
+                }
+            });
+        },
+        /**
+         * Expand if collapsed, Collapse if expanded
+         * 
+         * @returns {Node}
+         */
+        toggle: function() {
+            var $this = $(this);
+            if ($this.treegrid('isExpanded')) {
+                $this.treegrid('collapse');
+            } else {
+                $this.treegrid('expand');
+            }
+            return $this;
+        },
+        /**
+         * Rendering node
+         * 
+         * @returns {Node}
+         */
+        render: function() {
+            return $(this).each(function() {
+                var $this = $(this);
+
+                if ($this.treegrid('isOneOfParentsCollapsed')) {
+                    $this.hide();
+                } else {
+                    $this.show();
+                }
+                if (!$this.treegrid('isLeaf')) {
+                    $this.treegrid('renderExpander');
+                    $this.treegrid('getChildNodes').treegrid('render');
+                }
+            });
+        },
+        /**
+         * Rendering expander depends on node state
+         * 
+         * @returns {Node}
+         */
+        renderExpander: function() {
+            return $(this).each(function() {
+                var $this = $(this);
+                var expander = $this.treegrid('getSetting', 'getExpander').apply(this);
+                if (expander) {
+
+                    if (!$this.treegrid('isCollapsed')) {
+                        expander.removeClass($this.treegrid('getSetting', 'expanderCollapsedClass'));
+                        expander.addClass($this.treegrid('getSetting', 'expanderExpandedClass'));
+                    } else {
+                        expander.removeClass($this.treegrid('getSetting', 'expanderExpandedClass'));
+                        expander.addClass($this.treegrid('getSetting', 'expanderCollapsedClass'));
+                    }
+                } else {
+                    $this.treegrid('initExpander');
+                    $this.treegrid('renderExpander');
+                }
+            });
+        }
+    };
+    $.fn.treegrid = function(method) {
+        if (methods[method]) {
+            return methods[ method ].apply(this, Array.prototype.slice.call(arguments, 1));
+        } else if (typeof method === 'object' || !method) {
+            return methods.initTree.apply(this, arguments);
+        } else {
+            $.error('Method with name ' + method + ' does not exists for jQuery.treegrid');
+        }
+    };
+    /**
+     *  Plugin's default options
+     */
+    $.fn.treegrid.defaults = {
+        initialState: 'expanded',
+        saveState: false,
+        saveStateMethod: 'cookie',
+        saveStateName: 'tree-grid-state',
+        expanderTemplate: '<span class="treegrid-expander"></span>',
+        indentTemplate: '<span class="treegrid-indent"></span>',
+        expanderExpandedClass: 'treegrid-expander-expanded',
+        expanderCollapsedClass: 'treegrid-expander-collapsed',
+        treeColumn: 0,
+        getExpander: function() {
+            return $(this).find('.treegrid-expander');
+        },
+        getNodeId: function() {
+            var template = /treegrid-([A-Za-z0-9_-]+)/;
+            if (template.test($(this).attr('class'))) {
+                return template.exec($(this).attr('class'))[1];
+            }
+            return null;
+        },
+        getParentNodeId: function() {
+            var template = /treegrid-parent-([A-Za-z0-9_-]+)/;
+            if (template.test($(this).attr('class'))) {
+                return template.exec($(this).attr('class'))[1];
+            }
+            return null;
+        },
+        getNodeById: function(id, treegridContainer) {
+            var templateClass = "treegrid-" + id;
+            return treegridContainer.find('tr.' + templateClass);
+        },
+        getChildNodes: function(id, treegridContainer) {
+            var templateClass = "treegrid-parent-" + id;
+            return treegridContainer.find('tr.' + templateClass);
+        },
+        getTreeGridContainer: function() {
+            return $(this).closest('table');
+        },
+        getRootNodes: function(treegridContainer) {
+            var result = $.grep(treegridContainer.find('tr'), function(element) {
+                var classNames = $(element).attr('class');
+                var templateClass = /treegrid-([A-Za-z0-9_-]+)/;
+                var templateParentClass = /treegrid-parent-([A-Za-z0-9_-]+)/;
+                return templateClass.test(classNames) && !templateParentClass.test(classNames);
+            });
+            return $(result);
+        },
+        getAllNodes: function(treegridContainer) {
+            var result = $.grep(treegridContainer.find('tr'), function(element) {
+                var classNames = $(element).attr('class');
+                var templateClass = /treegrid-([A-Za-z0-9_-]+)/;
+                return templateClass.test(classNames);
+            });
+            return $(result);
+        },
+        //Events
+        onCollapse: null,
+        onExpand: null,
+        onChange: null
+
+    };
+})(jQuery);

Some files were not shown because too many files changed in this diff