Index: /extensions/EasyCaptcha/admin.php
===================================================================
--- /extensions/EasyCaptcha/admin.php	(revision 24215)
+++ /extensions/EasyCaptcha/admin.php	(revision 24215)
@@ -0,0 +1,106 @@
+<?php
+defined('EASYCAPTCHA_ID') or die('Hacking attempt!');
+
+global $pwg_loaded_plugins;
+$loaded = array(
+  'contactform' => isset($pwg_loaded_plugins['ContactForm']),
+  'category' => isset($pwg_loaded_plugins['Comments_on_Albums']),
+  'guestbook' => isset($pwg_loaded_plugins['GuestBook']),
+  'cryptocaptcha' => isset($pwg_loaded_plugins['CryptograPHP']),
+  );
+
+if ($loaded['cryptocaptcha'])
+{
+  $page['warnings'][] = l10n('easycaptcha_cryptocaptcha');
+}
+
+if (isset($_POST['submit']))
+{
+  $conf['EasyCaptcha'] = array(
+    'activate_on'     => array(
+      'picture'     => in_array('picture', $_POST['activate_on']),
+      'category'    => in_array('category', $_POST['activate_on']) || !$loaded['category'],
+      'register'    => in_array('register', $_POST['activate_on']),
+      'contactform' => in_array('contactform', $_POST['activate_on']) || !$loaded['contactform'],
+      'guestbook'   => in_array('guestbook', $_POST['activate_on']) || !$loaded['guestbook'],
+      ),
+    'comments_action' => $_POST['comments_action'],
+    'challenge' => $_POST['challenge'],
+    'drag' => array(
+      'theme' => $_POST['drag']['theme'],
+      'size'  => (int)$_POST['drag']['size'],
+      'nb'    => (int)$_POST['drag']['nb'],
+      'bg1'   => check_color($_POST['drag']['bg1']),
+      'bg2'   => check_color($_POST['drag']['bg2']),
+      'obj'   => check_color($_POST['drag']['obj']),
+      'sel'   => check_color($_POST['drag']['sel']),
+      'bd1'   => check_color($_POST['drag']['bd1']),
+      'bd2'   => check_color($_POST['drag']['bd2']),
+      'txt'   => check_color($_POST['drag']['txt']),
+      ),
+    'tictac' => array(
+      'size'  => (int)$_POST['tictac']['size'],
+      'bg1'   => check_color($_POST['tictac']['bg1']),
+      'bg2'   => check_color($_POST['tictac']['bg2']),
+      'bd'    => check_color($_POST['tictac']['bd']),
+      'obj'   => check_color($_POST['tictac']['obj']),
+      'sel'   => check_color($_POST['tictac']['sel']),
+      ),
+    );
+
+  conf_update_param('EasyCaptcha', serialize($conf['EasyCaptcha']));
+  array_push($page['infos'], l10n('Information data registered in database'));
+}
+
+function list_themes($dir)
+{
+  $dir = rtrim($dir, '/');
+  $dh = opendir($dir);
+  $themes = array();
+
+  while (($item = readdir($dh)) !== false )
+  {
+    if ($item!=='.' && $item!=='..' && file_exists($dir.'/'.$item.'/conf.inc.php'))
+    {
+      $drag_images = include($dir.'/'.$item.'/conf.inc.php');
+      $themes[$item] = array(
+        'image' => key($drag_images),
+        'count' => count($drag_images),
+        );
+    }
+  }
+
+  closedir($dh);
+  return $themes;
+}
+
+$template->assign(array(
+  'easycaptcha' => $conf['EasyCaptcha'],
+  'loaded' => $loaded,
+  'THEMES' => list_themes(EASYCAPTCHA_PATH.'drag'),
+  'EASYCAPTCHA_PATH' => EASYCAPTCHA_PATH,
+  'EASYCAPTCHA_ABS_PATH' => realpath(EASYCAPTCHA_PATH).'/',
+  ));
+
+$template->set_filename('plugin_admin_content', realpath(EASYCAPTCHA_PATH.'template/admin.tpl'));
+$template->assign_var_from_handle('ADMIN_CONTENT', 'plugin_admin_content');
+
+
+function check_color($hex)
+{
+  global $page;
+
+  $hex = ltrim($hex, '#');
+
+  if (strlen($hex) == 3)
+  {
+    $hex = $hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2];
+  }
+  else if (strlen($hex) != 6)
+  {
+    $page['errors'][] = sprintf(l10n('Invalid color code <i>%s</i>'), '#'.$hex);
+    $hex = '000000';
+  }
+
+  return '#'.$hex;
+}
Index: /extensions/EasyCaptcha/drag/functions_drag.inc.php
===================================================================
--- /extensions/EasyCaptcha/drag/functions_drag.inc.php	(revision 24215)
+++ /extensions/EasyCaptcha/drag/functions_drag.inc.php	(revision 24215)
@@ -0,0 +1,94 @@
+<?php
+defined('EASYCAPTCHA_ID') or die('Hacking attempt!');
+
+/*
+ * crypt the name of an image, it use the Piwigo secret_key
+ * and a random salt to prevent attacker to build a dictionnary
+ */
+function easycaptcha_encode_image_url($name)
+{
+  global $conf, $easycaptcha_uniqid;
+
+  if (empty($easycaptcha_uniqid))
+  {
+    $easycaptcha_uniqid = uniqid(null, true);
+  }
+
+  $name.= '-'. $easycaptcha_uniqid;
+  $name = simple_crypt($name, $conf['secret_key']);
+
+  return $name;
+}
+
+/*
+ * decrypt the image name
+ */
+function easycaptcha_decode_image_url($name)
+{
+  global $conf;
+
+  $name = simple_decrypt($name, $conf['secret_key']);
+  $name = strtok($name, '-');
+
+  return $name;
+}
+
+/**
+ * crypt a string using
+ * http://stackoverflow.com/questions/800922/how-to-encrypt-string-without-mcrypt-library-in-php/802957#802957
+ * @param: string value to crypt
+ * @param: string key
+ * @return: string
+ */
+function simple_crypt($value, $key)
+{
+  $result = null;
+  for($i = 0; $i < strlen($value); $i++)
+  {
+    $char = substr($value, $i, 1);
+    $keychar = substr($key, ($i % strlen($key))-1, 1);
+    $char = chr(ord($char) + ord($keychar));
+    $result .= $char;
+  }
+
+  $result = base64url_encode($result);
+  return trim($result);
+}
+
+/**
+ * decrypt a string crypted with previous function
+ * @param: string value to decrypt
+ * @param: string key
+ * @return: string
+ */
+function simple_decrypt($value, $key)
+{
+  $value = base64url_decode($value);
+
+  $result = null;
+  for($i = 0; $i < strlen($value); $i++)
+  {
+    $char = substr($value, $i, 1);
+    $keychar = substr($key, ($i % strlen($key))-1, 1);
+    $char = chr(ord($char) - ord($keychar));
+    $result .= $char;
+  }
+
+  return trim($result);
+}
+
+/**
+ * variant of base64 functions usable into url
+ * http://php.net/manual/en/function.base64-encode.php#103849
+ */
+if (!function_exists('base64url_encode'))
+{
+  function base64url_encode($data)
+  {
+    return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
+  }
+  function base64url_decode($data)
+  {
+    return base64_decode(str_pad(strtr($data, '-_', '+/'), strlen($data) % 4, '=', STR_PAD_RIGHT));
+  }
+}
Index: /extensions/EasyCaptcha/drag/get.php
===================================================================
--- /extensions/EasyCaptcha/drag/get.php	(revision 24215)
+++ /extensions/EasyCaptcha/drag/get.php	(revision 24215)
@@ -0,0 +1,21 @@
+<?php
+define('PHPWG_ROOT_PATH', '../../../');
+include(PHPWG_ROOT_PATH . 'include/common.inc.php');
+
+defined('EASYCAPTCHA_ID') or die('Hacking attempt!');
+include_once(EASYCAPTCHA_PATH . 'drag/functions_drag.inc.php');
+
+if (count($_GET) != 2) die('Hacking attempt!');
+
+list($theme, $image) = array_keys($_GET);
+
+$image = easycaptcha_decode_image_url($image);
+
+if (@file_exists(EASYCAPTCHA_PATH.'drag/'.$theme.'/'.$image))
+{
+  $ext = get_extension($image);
+  if ($ext == 'jpg') $ext = 'jpeg';
+
+  header('Content-Type: image/'.$ext);
+  readfile(EASYCAPTCHA_PATH.'drag/'.$theme.'/'.$image);
+}
Index: /extensions/EasyCaptcha/drag/icons/conf.inc.php
===================================================================
--- /extensions/EasyCaptcha/drag/icons/conf.inc.php	(revision 24215)
+++ /extensions/EasyCaptcha/drag/icons/conf.inc.php	(revision 24215)
@@ -0,0 +1,32 @@
+<?php
+
+// https://www.iconfinder.com/iconsets/ie_Bright
+// https://www.iconfinder.com/iconsets/UrbanStories-png-Artdesigner-lv
+// https://www.iconfinder.com/iconsets/crystalproject
+return array(
+  'house.png' => 'house',
+  'ball.png' => 'ball',
+  'battery.png' => 'battery',
+  'bicycle.png' => 'bicycle',
+  'binoculars.png' => 'binoculars',
+  'book.png' => 'book',
+  'calculator.png' => 'calculator',
+  'calendar.png' => 'calendar',
+  'camera.png' => 'camera',
+  'car.png' => 'car',
+  'cart.png' => 'shopping cart',
+  'chest.png' => 'chest',
+  'cookie.png' => 'cookie',
+  'scissors.png' => 'scissors',
+  'dvd.png' => 'DVD',
+  'disk.png' => 'disk',
+  'guitar.png' => 'guitar',
+  'hat.png' => 'hat',
+  'mobile.png' => 'mobile phone',
+  'pencil.png' => 'pencil',
+  'printer.png' => 'printer',
+  'search.png' => 'magnifying glass',
+  'shoe.png' => 'shoe',
+  'suitcase.png' => 'suitcase',
+  'trash.png' => 'trash',
+  );
Index: /extensions/EasyCaptcha/drag/icons/index.php
===================================================================
--- /extensions/EasyCaptcha/drag/icons/index.php	(revision 24215)
+++ /extensions/EasyCaptcha/drag/icons/index.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
Index: /extensions/EasyCaptcha/drag/index.php
===================================================================
--- /extensions/EasyCaptcha/drag/index.php	(revision 24215)
+++ /extensions/EasyCaptcha/drag/index.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
Index: /extensions/EasyCaptcha/drag/pets/conf.inc.php
===================================================================
--- /extensions/EasyCaptcha/drag/pets/conf.inc.php	(revision 24215)
+++ /extensions/EasyCaptcha/drag/pets/conf.inc.php	(revision 24215)
@@ -0,0 +1,23 @@
+<?php
+
+// https://www.iconfinder.com/iconsets/cutecritters
+// https://www.iconfinder.com/iconsets/crystalproject
+return array(
+  'dog.png' => 'dog',
+  'bat.png' => 'bat',
+  'butterfly.png' => 'butterfly',
+  'cat.png' => 'cat',
+  'duck.png' => 'duck',
+  'eagle.png' => 'eagle',
+  'elephant.png' => 'elephant',
+  'fox.png' => 'fox',
+  'frog.png' => 'frog',
+  'horse.png' => 'horse',
+  'ladybug.png' => 'ladybug',
+  'lion.png' => 'lion',
+  'mouse.png' => 'mouse',
+  'panda.png' => 'panda',
+  'penguin.png' => 'penguin',
+  'snake.png' => 'snake',
+  'toucan.png' => 'toucan',
+  );
Index: /extensions/EasyCaptcha/drag/pets/index.php
===================================================================
--- /extensions/EasyCaptcha/drag/pets/index.php	(revision 24215)
+++ /extensions/EasyCaptcha/drag/pets/index.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
Index: /extensions/EasyCaptcha/include/category.inc.php
===================================================================
--- /extensions/EasyCaptcha/include/category.inc.php	(revision 24215)
+++ /extensions/EasyCaptcha/include/category.inc.php	(revision 24215)
@@ -0,0 +1,33 @@
+<?php
+defined('EASYCAPTCHA_ID') or die('Hacking attempt!');
+
+include(EASYCAPTCHA_PATH.'include/common.inc.php');
+add_event_handler('loc_begin_index', 'add_easycaptcha');
+add_event_handler('user_comment_check_albums', 'check_easycaptcha', EVENT_HANDLER_PRIORITY_NEUTRAL, 2);
+
+function add_easycaptcha()
+{
+  global $template;
+  $template->set_prefilter('comments_on_albums', 'prefilter_easycaptcha');
+}
+
+function prefilter_easycaptcha($content, $smarty)
+{
+  $search = '{$comment_add.CONTENT}</textarea></p>';
+  return str_replace($search, $search."\n{\$EASYCAPTCHA}", $content);
+}
+
+function check_easycaptcha($action, $comment)
+{
+  global $conf, $page;
+
+  if (!is_a_guest()) return $action;
+
+  if (!easycaptcha_check())
+  {
+    if ($conf['EasyCaptcha']['comments_action'] == 'reject')  $page['errors'][] = l10n('Invalid answer');
+    return ($action != 'reject') ? $conf['EasyCaptcha']['comments_action'] : 'reject';
+  }
+
+  return $action;
+}
Index: /extensions/EasyCaptcha/include/common.inc.php
===================================================================
--- /extensions/EasyCaptcha/include/common.inc.php	(revision 24215)
+++ /extensions/EasyCaptcha/include/common.inc.php	(revision 24215)
@@ -0,0 +1,81 @@
+<?php
+defined('EASYCAPTCHA_ID') or die('Hacking attempt!');
+
+include_once(EASYCAPTCHA_PATH . 'drag/functions_drag.inc.php');
+
+global $template;
+
+// choose random challenge
+if ($conf['EasyCaptcha']['challenge'] == 'random')
+{
+  $challenges = array('tictac', 'drag');
+  $conf['EasyCaptcha']['challenge'] = $challenges[ rand(0,1) ];
+}
+
+// Drag & drop
+if ($conf['EasyCaptcha']['challenge'] == 'drag')
+{
+  load_language($conf['EasyCaptcha']['drag']['theme'].'.lang', EASYCAPTCHA_PATH);
+
+  $drag_images = include(EASYCAPTCHA_PATH.'drag/'.$conf['EasyCaptcha']['drag']['theme'].'/conf.inc.php');
+  $conf['EasyCaptcha']['drag']['nb'] = min($conf['EasyCaptcha']['drag']['nb'], count($drag_images));
+
+  foreach (array_rand($drag_images, $conf['EasyCaptcha']['drag']['nb']) as $row)
+  {
+    $conf['EasyCaptcha']['drag']['selection'][ $row ] = easycaptcha_encode_image_url($row);
+  }
+
+  $conf['EasyCaptcha']['drag']['selected'] = array_rand($conf['EasyCaptcha']['drag']['selection']);
+  $conf['EasyCaptcha']['drag']['text']     = l10n($drag_images[ $conf['EasyCaptcha']['drag']['selected'] ]);
+  $conf['EasyCaptcha']['drag']['key']      = $conf['EasyCaptcha']['challenge'] .'-'. pwg_password_hash($conf['secret_key'] . $conf['EasyCaptcha']['drag']['selected']);
+
+  $template->assign('EASYCAPTCHA_CONF', $conf['EasyCaptcha']['drag']);
+}
+// Tic-tac-toe
+else if ($conf['EasyCaptcha']['challenge'] == 'tictac')
+{
+  $conf['EasyCaptcha']['tictac']['key'] = $conf['EasyCaptcha']['challenge'] .'-0';
+  $template->assign('EASYCAPTCHA_CONF', $conf['EasyCaptcha']['tictac']);
+}
+else
+{
+  return;
+}
+
+load_language('plugin.lang', EASYCAPTCHA_PATH);
+
+$template->assign(array(
+  'EASYCAPTCHA_CHALLENGE' => $conf['EasyCaptcha']['challenge'],
+  'EASYCAPTCHA_PATH' => EASYCAPTCHA_PATH,
+  'EASYCAPTCHA_ABS_PATH' => realpath(EASYCAPTCHA_PATH).'/',
+  ));
+
+$template->set_filename('EasyCaptcha', realpath(EASYCAPTCHA_PATH.'template/'.$conf['EasyCaptcha']['template'].'.tpl'));
+$template->assign_var_from_handle('EASYCAPTCHA', 'EasyCaptcha');
+
+
+function easycaptcha_check()
+{
+  global $conf;
+
+  if (empty($_POST['easycaptcha_key']) || empty($_POST['easycaptcha']))
+  {
+    return false;
+  }
+
+  list($challenge, $key) = explode('-', $_POST['easycaptcha_key']);
+
+  if ($challenge == 'drag')
+  {
+    $check = easycaptcha_decode_image_url($_POST['easycaptcha']);
+    return pwg_password_verify($conf['secret_key'] . $check, $key);
+  }
+  else if ($challenge == 'tictac')
+  {
+    return $_POST['easycaptcha'] == pwg_get_session_var('easycaptcha', '33');
+  }
+  else
+  {
+    return false;
+  }
+}
Index: /extensions/EasyCaptcha/include/contactform.inc.php
===================================================================
--- /extensions/EasyCaptcha/include/contactform.inc.php	(revision 24215)
+++ /extensions/EasyCaptcha/include/contactform.inc.php	(revision 24215)
@@ -0,0 +1,33 @@
+<?php
+defined('EASYCAPTCHA_ID') or die('Hacking attempt!');
+
+include(EASYCAPTCHA_PATH.'include/common.inc.php');
+add_event_handler('loc_begin_index', 'add_easycaptcha');
+add_event_handler('contact_form_check', 'check_easycaptcha', EVENT_HANDLER_PRIORITY_NEUTRAL, 2);
+
+function add_easycaptcha()
+{
+  global $template;
+  $template->set_prefilter('index', 'prefilter_easycaptcha');
+}
+
+function prefilter_easycaptcha($content, $smarty)
+{
+  $search = '{$contact.content}</textarea></td>';
+  return str_replace($search, $search."\n{\$EASYCAPTCHA}", $content);
+}
+
+function check_easycaptcha($action, $comment)
+{
+  global $conf, $page;
+
+  if (!is_a_guest()) return $action;
+
+  if (!easycaptcha_check())
+  {
+    $page['errors'][] = l10n('Invalid answer');
+    return 'reject';
+  }
+
+  return $action;
+}
Index: /extensions/EasyCaptcha/include/guestbook.inc.php
===================================================================
--- /extensions/EasyCaptcha/include/guestbook.inc.php	(revision 24215)
+++ /extensions/EasyCaptcha/include/guestbook.inc.php	(revision 24215)
@@ -0,0 +1,34 @@
+<?php
+defined('EASYCAPTCHA_ID') or die('Hacking attempt!');
+
+include(EASYCAPTCHA_PATH.'include/common.inc.php');
+add_event_handler('loc_begin_index', 'add_easycaptcha');
+add_event_handler('user_comment_check_guestbook', 'check_easycaptcha', EVENT_HANDLER_PRIORITY_NEUTRAL, 2);
+
+function add_easycaptcha()
+{
+  global $template;
+  $template->set_prefilter('index', 'prefilter_easycaptcha');
+}
+
+function prefilter_easycaptcha($content, $smarty)
+{
+  $search = '#{\$comment_add\.CONTENT}</textarea>(\s*)</td>(\s*)</tr>#';
+  $replace = '{\$comment_add.CONTENT}</textarea>$1</td>$2</tr>'."\n".'{\$EASYCAPTCHA}';
+  return preg_replace($search, $replace, $content);
+}
+
+function check_easycaptcha($action, $comment)
+{
+  global $conf, $page;
+
+  if (!is_a_guest()) return $action;
+
+  if (!easycaptcha_check())
+  {
+    $page['errors'][] = l10n('Invalid answer');
+    return 'reject';
+  }
+
+  return $action;
+}
Index: /extensions/EasyCaptcha/include/index.php
===================================================================
--- /extensions/EasyCaptcha/include/index.php	(revision 24215)
+++ /extensions/EasyCaptcha/include/index.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
Index: /extensions/EasyCaptcha/include/install.inc.php
===================================================================
--- /extensions/EasyCaptcha/include/install.inc.php	(revision 24215)
+++ /extensions/EasyCaptcha/include/install.inc.php	(revision 24215)
@@ -0,0 +1,45 @@
+<?php
+defined('EASYCAPTCHA_ID') or die('Hacking attempt!');
+
+function easycaptcha_install()
+{
+  global $conf;
+
+  if (empty($conf['EasyCaptcha']))
+  {
+    $easycaptcha_default_config = array(
+      'activate_on' => array(
+        'picture'     => true,
+        'category'    => true,
+        'register'    => true,
+        'contactform' => true,
+        'guestbook'   => true,
+        ),
+      'comments_action' => 'reject',
+      'challenge' => 'random',
+      'drag' => array(
+        'theme' => 'icons',
+        'size'  => 50,
+        'nb'    => 5,
+        'bg1'   => '#F7F7F7',
+        'bg2'   => '#E5E5E5',
+        'obj'   => '#FFFFFF',
+        'sel'   => '#C8FF96',
+        'bd1'   => '#DDDDDD',
+        'bd2'   => '#555555',
+        'txt'   => '#222222',
+        ),
+      'tictac' => array(
+        'size'  => 128,
+        'bg1'   => '#F7F7F7',
+        'bg2'   => '#E5E5E5',
+        'bd'    => '#DDDDDD',
+        'obj'   => '#00B4F7',
+        'sel'   => '#F7B400',
+        ),
+      );
+
+    $conf['EasyCaptcha'] = serialize($easycaptcha_default_config);
+    conf_update_param('EasyCaptcha', $conf['EasyCaptcha']);
+  }
+}
Index: /extensions/EasyCaptcha/include/picture.inc.php
===================================================================
--- /extensions/EasyCaptcha/include/picture.inc.php	(revision 24215)
+++ /extensions/EasyCaptcha/include/picture.inc.php	(revision 24215)
@@ -0,0 +1,33 @@
+<?php
+defined('EASYCAPTCHA_ID') or die('Hacking attempt!');
+
+include(EASYCAPTCHA_PATH.'include/common.inc.php');
+add_event_handler('loc_end_picture', 'add_easycaptcha');
+add_event_handler('user_comment_check', 'check_easycaptcha', EVENT_HANDLER_PRIORITY_NEUTRAL, 2);
+
+function add_easycaptcha()
+{
+  global $template;
+  $template->set_prefilter('picture', 'prefilter_easycaptcha');
+}
+
+function prefilter_easycaptcha($content, $smarty)
+{
+  $search = '{$comment_add.CONTENT}</textarea></p>';
+  return str_replace($search, $search."\n{\$EASYCAPTCHA}", $content);
+}
+
+function check_easycaptcha($action, $comment)
+{
+  global $conf, $page;
+
+  if (!is_a_guest()) return $action;
+
+  if (!easycaptcha_check())
+  {
+    if ($conf['EasyCaptcha']['comments_action'] == 'reject') $page['errors'][] = l10n('Invalid answer');
+    return ($action != 'reject') ? $conf['EasyCaptcha']['comments_action'] : 'reject';
+  }
+
+  return $action;
+}
Index: /extensions/EasyCaptcha/include/register.inc.php
===================================================================
--- /extensions/EasyCaptcha/include/register.inc.php	(revision 24215)
+++ /extensions/EasyCaptcha/include/register.inc.php	(revision 24215)
@@ -0,0 +1,28 @@
+<?php
+defined('EASYCAPTCHA_ID') or die('Hacking attempt!');
+
+include(EASYCAPTCHA_PATH.'include/common.inc.php');
+add_event_handler('loc_end_page_header', 'add_easycaptcha');
+add_event_handler('register_user_check', 'check_easycaptcha');
+
+function add_easycaptcha()
+{
+  global $template;
+  $template->set_prefilter('register', 'prefilter_easycaptcha');
+}
+
+function prefilter_easycaptcha($content, $smarty)
+{
+  $search = '<input type="checkbox" name="send_password_by_mail" id="send_password_by_mail" value="1" checked="checked">';
+  return str_replace($search, $search."\n{\$EASYCAPTCHA}", $content);
+}
+
+function check_easycaptcha($errors)
+{
+  if (!easycaptcha_check())
+  {
+    $errors[] = l10n('Invalid answer');
+  }
+
+  return $errors;
+}
Index: /extensions/EasyCaptcha/index.php
===================================================================
--- /extensions/EasyCaptcha/index.php	(revision 24215)
+++ /extensions/EasyCaptcha/index.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
Index: /extensions/EasyCaptcha/language/ca_ES/index.php
===================================================================
--- /extensions/EasyCaptcha/language/ca_ES/index.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/ca_ES/index.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
Index: /extensions/EasyCaptcha/language/ca_ES/plugin.lang.php
===================================================================
--- /extensions/EasyCaptcha/language/ca_ES/plugin.lang.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/ca_ES/plugin.lang.php	(revision 24215)
@@ -0,0 +1,10 @@
+<?php
+$lang['Activate on'] = 'Activa en';
+$lang['Preview'] = 'Previsualització';
+$lang['Register form'] = 'Formulari de registre';
+$lang['Album comments'] = 'Comentaris dels àlbums';
+$lang['Comments action'] = 'Acció dels comentaris';
+$lang['Guestbook'] = 'Llibre de visites';
+$lang['Moderate'] = 'Moderat';
+$lang['Picture comments'] = 'Comentaris de la imatge';
+?>
Index: /extensions/EasyCaptcha/language/cs_CZ/index.php
===================================================================
--- /extensions/EasyCaptcha/language/cs_CZ/index.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/cs_CZ/index.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
Index: /extensions/EasyCaptcha/language/cs_CZ/plugin.lang.php
===================================================================
--- /extensions/EasyCaptcha/language/cs_CZ/plugin.lang.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/cs_CZ/plugin.lang.php	(revision 24215)
@@ -0,0 +1,11 @@
+<?php
+$lang['Comments action'] = 'Komentáře akce';
+$lang['Moderate'] = 'Moderovat';
+$lang['Preview'] = 'předběžný náhled';
+$lang['Activate on'] = 'Aktivovat na';
+$lang['Album comments'] = 'Komentáře k albům';
+$lang['Contact form'] = 'Kontaktní formulář';
+$lang['Guestbook'] = 'Knihu návštěv';
+$lang['Picture comments'] = 'Komentáře k obrázkům';
+$lang['Register form'] = 'Registrační formulář';
+?>
Index: /extensions/EasyCaptcha/language/da_DK/index.php
===================================================================
--- /extensions/EasyCaptcha/language/da_DK/index.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/da_DK/index.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
Index: /extensions/EasyCaptcha/language/da_DK/plugin.lang.php
===================================================================
--- /extensions/EasyCaptcha/language/da_DK/plugin.lang.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/da_DK/plugin.lang.php	(revision 24215)
@@ -0,0 +1,11 @@
+<?php
+$lang['Preview'] = 'Forhåndsvisning';
+$lang['Register form'] = 'Registreringsformular';
+$lang['Activate on'] = 'Aktivér ved';
+$lang['Album comments'] = 'Albumkommentarer';
+$lang['Comments action'] = 'Kommentarhandling';
+$lang['Contact form'] = 'Kontaktformular';
+$lang['Guestbook'] = 'Gæstebog';
+$lang['Moderate'] = 'Moderér';
+$lang['Picture comments'] = 'Billedkommentarer';
+?>
Index: /extensions/EasyCaptcha/language/de_DE/index.php
===================================================================
--- /extensions/EasyCaptcha/language/de_DE/index.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/de_DE/index.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
Index: /extensions/EasyCaptcha/language/de_DE/plugin.lang.php
===================================================================
--- /extensions/EasyCaptcha/language/de_DE/plugin.lang.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/de_DE/plugin.lang.php	(revision 24215)
@@ -0,0 +1,11 @@
+<?php
+$lang['Comments action'] = 'Kommentare Aktion';
+$lang['Moderate'] = 'Moderate';
+$lang['Preview'] = 'Vorschau';
+$lang['Activate on'] = 'Aktivierung EIN';
+$lang['Album comments'] = 'Album Inhalte';
+$lang['Contact form'] = 'Kontakt Formular';
+$lang['Guestbook'] = 'Gästebuch';
+$lang['Picture comments'] = 'Bilder Kommentare';
+$lang['Register form'] = 'Registrierungs-Formular ';
+?>
Index: /extensions/EasyCaptcha/language/el_GR/index.php
===================================================================
--- /extensions/EasyCaptcha/language/el_GR/index.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/el_GR/index.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
Index: /extensions/EasyCaptcha/language/el_GR/plugin.lang.php
===================================================================
--- /extensions/EasyCaptcha/language/el_GR/plugin.lang.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/el_GR/plugin.lang.php	(revision 24215)
@@ -0,0 +1,11 @@
+<?php
+$lang['Comments action'] = 'Ενέργεια σχολίου';
+$lang['Moderate'] = 'Μέτριο';
+$lang['Preview'] = 'Προεπισκόπηση';
+$lang['Activate on'] = 'Ενεργοποίηση για';
+$lang['Album comments'] = 'Σχόλια λευκώματος';
+$lang['Contact form'] = 'Φόρμα επικοινωνίας';
+$lang['Guestbook'] = 'Βιβλίο Επισκεπτών';
+$lang['Picture comments'] = 'Σχόλια εικόνας';
+$lang['Register form'] = 'Φόρμα Εγγραφής';
+?>
Index: /extensions/EasyCaptcha/language/en_UK/description.txt
===================================================================
--- /extensions/EasyCaptcha/language/en_UK/description.txt	(revision 24215)
+++ /extensions/EasyCaptcha/language/en_UK/description.txt	(revision 24215)
@@ -0,0 +1,1 @@
+An fun antibot system for comments, registration, ContactForm and GuestBook.
Index: /extensions/EasyCaptcha/language/en_UK/icons.lang.php
===================================================================
--- /extensions/EasyCaptcha/language/en_UK/icons.lang.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/en_UK/icons.lang.php	(revision 24215)
@@ -0,0 +1,27 @@
+<?php
+$lang['ball'] = 'ball';
+$lang['battery'] = 'battery';
+$lang['bicycle'] = 'bicycle';
+$lang['binoculars'] = 'binoculars';
+$lang['book'] = 'book';
+$lang['calculator'] = 'calculator';
+$lang['calendar'] = 'calendar';
+$lang['camera'] = 'camera';
+$lang['car'] = 'car';
+$lang['shopping cart'] = 'shopping cart';
+$lang['chest'] = 'chest';
+$lang['cookie'] = 'cookie';
+$lang['scissors'] = 'scissors';
+$lang['DVD'] = 'DVD';
+$lang['disk'] = 'disk';
+$lang['guitar'] = 'guitar';
+$lang['hat'] = 'hat';
+$lang['house'] = 'house';
+$lang['mobile phone'] = 'mobile phone';
+$lang['pencil'] = 'pencil';
+$lang['printer'] = 'printer';
+$lang['magnifying glass'] = 'magnifying glass';
+$lang['shoe'] = 'shoe';
+$lang['suitcase'] = 'suitcase';
+$lang['trash'] = 'trash';
+?>
Index: /extensions/EasyCaptcha/language/en_UK/index.php
===================================================================
--- /extensions/EasyCaptcha/language/en_UK/index.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/en_UK/index.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
Index: /extensions/EasyCaptcha/language/en_UK/pets.lang.php
===================================================================
--- /extensions/EasyCaptcha/language/en_UK/pets.lang.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/en_UK/pets.lang.php	(revision 24215)
@@ -0,0 +1,19 @@
+<?php
+$lang['bat'] = 'bat';
+$lang['butterfly'] = 'butterfly';
+$lang['cat'] = 'cat';
+$lang['dog'] = 'dog';
+$lang['duck'] = 'duck';
+$lang['eagle'] = 'eagle';
+$lang['elephant'] = 'elephant';
+$lang['fox'] = 'fox';
+$lang['frog'] = 'frog';
+$lang['horse'] = 'horse';
+$lang['ladybug'] = 'ladybug';
+$lang['lion'] = 'lion';
+$lang['mouse'] = 'mouse';
+$lang['panda'] = 'panda';
+$lang['penguin'] = 'penguin';
+$lang['snake'] = 'snake';
+$lang['toucan'] = 'toucan';
+?>
Index: /extensions/EasyCaptcha/language/en_UK/plugin.lang.php
===================================================================
--- /extensions/EasyCaptcha/language/en_UK/plugin.lang.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/en_UK/plugin.lang.php	(revision 24215)
@@ -0,0 +1,37 @@
+<?php
+$lang['Comments action'] = 'Comments action';
+$lang['Moderate'] = 'Moderate';
+$lang['Activate on'] = 'Activate on';
+$lang['Picture comments'] = 'Picture comments';
+$lang['Album comments'] = 'Album comments';
+$lang['Register form'] = 'Register form';
+$lang['Contact form'] = 'Contact form';
+$lang['Guestbook'] = 'Guestbook';
+$lang['Preview'] = 'Preview';
+$lang['Background'] = 'Background';
+$lang['Border'] = 'Border';
+$lang['Challenge'] = 'Challenge';
+$lang['Colors'] = 'Colors';
+$lang['Drag & drop options'] = 'Drag & drop options';
+$lang['Drop'] = 'Drop';
+$lang['Drop border'] = 'Drop border';
+$lang['Image'] = 'Image';
+$lang['Image border'] = 'Image border';
+$lang['Image drag & drop'] = 'Image drag & drop';
+$lang['Image size'] = 'Image size';
+$lang['Invalid answer'] = 'Invalid answer';
+$lang['Marks'] = 'Marks';
+$lang['Number of images'] = 'Number of images';
+$lang['Pleaser answer'] = 'Pleaser answer';
+$lang['Random'] = 'Random';
+$lang['Text'] = 'Text';
+$lang['Theme'] = 'Theme';
+$lang['Invalid color code <i>%s</i>'] = 'Invalid color code <i>%s</i>';
+$lang['Tic-tac-toe minigame'] = 'Tic-tac-toe minigame';
+$lang['Tic-tac-toe options'] = 'Tic-tac-toe options';
+$lang['easycaptcha_drag_%s'] = 'To verify you are a human, please place the <b>%s</b> in the most right box bellow.';
+$lang['easycaptcha_tictac'] = 'You are player X, click on the right case to complete the line.';
+$lang['easycaptcha_javascript_public'] = 'You must activate JavaScript in your browser in order to be able to add a comment, sorry for the inconvenience.';
+$lang['easycaptcha_javascript_admin'] = 'This challenge requires that JavaScript is enabled on the visitor browser. About 1% of Internet users have Javascript disabled.';
+$lang['easycaptcha_cryptocaptcha'] = 'We detected that Crypto Captcha plugin is available on your gallery. Both plugins can be used at the same time, but you should not under any circumstances activate both of them on the same page.';
+?>
Index: /extensions/EasyCaptcha/language/es_ES/index.php
===================================================================
--- /extensions/EasyCaptcha/language/es_ES/index.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/es_ES/index.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
Index: /extensions/EasyCaptcha/language/es_ES/plugin.lang.php
===================================================================
--- /extensions/EasyCaptcha/language/es_ES/plugin.lang.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/es_ES/plugin.lang.php	(revision 24215)
@@ -0,0 +1,11 @@
+<?php
+$lang['Comments action'] = 'Acción para los Comentario';
+$lang['Moderate'] = 'Moderar';
+$lang['Preview'] = 'Previsualización';
+$lang['Activate on'] = 'Activar en ';
+$lang['Album comments'] = 'Comentarios sobre el álbum';
+$lang['Contact form'] = 'Formulario de contacto';
+$lang['Guestbook'] = 'Libro de visitas';
+$lang['Picture comments'] = 'Comentario de imagen';
+$lang['Register form'] = 'Formulario de registro';
+?>
Index: /extensions/EasyCaptcha/language/fa_IR/index.php
===================================================================
--- /extensions/EasyCaptcha/language/fa_IR/index.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/fa_IR/index.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
Index: /extensions/EasyCaptcha/language/fa_IR/plugin.lang.php
===================================================================
--- /extensions/EasyCaptcha/language/fa_IR/plugin.lang.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/fa_IR/plugin.lang.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$lang['Album comments'] = 'نظرات آلبوم';
+$lang['Picture comments'] = 'نظرات عکس';
+$lang['Register form'] = 'فرم ثبت نام';
+$lang['Preview'] = 'نمایش';
+$lang['Moderate'] = 'مدیریت';
+?>
Index: /extensions/EasyCaptcha/language/fr_FR/description.txt
===================================================================
--- /extensions/EasyCaptcha/language/fr_FR/description.txt	(revision 24215)
+++ /extensions/EasyCaptcha/language/fr_FR/description.txt	(revision 24215)
@@ -0,0 +1,1 @@
+Un système antibot ludique pour les commentaires, les enregistrements, ContactForm et GuestBook.
Index: /extensions/EasyCaptcha/language/fr_FR/icons.lang.php
===================================================================
--- /extensions/EasyCaptcha/language/fr_FR/icons.lang.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/fr_FR/icons.lang.php	(revision 24215)
@@ -0,0 +1,27 @@
+<?php
+$lang['ball'] = 'ballon';
+$lang['battery'] = 'pile';
+$lang['bicycle'] = 'vélo';
+$lang['binoculars'] = 'jumelles';
+$lang['book'] = 'libre';
+$lang['calculator'] = 'calculette';
+$lang['calendar'] = 'calendier';
+$lang['camera'] = 'appareil photo';
+$lang['car'] = 'voiture';
+$lang['shopping cart'] = 'caddie';
+$lang['chest'] = 'coffre';
+$lang['cookie'] = 'cookie';
+$lang['scissors'] = 'ciseaux';
+$lang['DVD'] = 'DVD';
+$lang['disk'] = 'disquette';
+$lang['guitar'] = 'guitare';
+$lang['hat'] = 'chapeau';
+$lang['house'] = 'maison';
+$lang['mobile phone'] = 'portable';
+$lang['pencil'] = 'crayon';
+$lang['printer'] = 'imprimante';
+$lang['magnifying glass'] = 'loupe';
+$lang['shoe'] = 'chaussure';
+$lang['suitcase'] = 'malette';
+$lang['trash'] = 'poubelle';
+?>
Index: /extensions/EasyCaptcha/language/fr_FR/index.php
===================================================================
--- /extensions/EasyCaptcha/language/fr_FR/index.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/fr_FR/index.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
Index: /extensions/EasyCaptcha/language/fr_FR/pets.lang.php
===================================================================
--- /extensions/EasyCaptcha/language/fr_FR/pets.lang.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/fr_FR/pets.lang.php	(revision 24215)
@@ -0,0 +1,19 @@
+<?php
+$lang['cat'] = 'chat';
+$lang['bat'] = 'chauve-souris';
+$lang['butterfly'] = 'papillon';
+$lang['dog'] = 'chien';
+$lang['duck'] = 'canard';
+$lang['eagle'] = 'aigle';
+$lang['elephant'] = 'éléphant';
+$lang['fox'] = 'renard';
+$lang['frog'] = 'grenouille';
+$lang['horse'] = 'cheval';
+$lang['ladybug'] = 'coccinelle';
+$lang['lion'] = 'lion';
+$lang['mouse'] = 'souris';
+$lang['panda'] = 'panda';
+$lang['penguin'] = 'pingouin';
+$lang['snake'] = 'serpent';
+$lang['toucan'] = 'toucan';
+?>
Index: /extensions/EasyCaptcha/language/fr_FR/plugin.lang.php
===================================================================
--- /extensions/EasyCaptcha/language/fr_FR/plugin.lang.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/fr_FR/plugin.lang.php	(revision 24215)
@@ -0,0 +1,37 @@
+<?php
+$lang['Comments action'] = 'Action pour les commentaires';
+$lang['Moderate'] = 'Modérer';
+$lang['Activate on'] = 'Activer sur';
+$lang['Picture comments'] = 'Commentaires photo';
+$lang['Album comments'] = 'Commentaire album';
+$lang['Register form'] = 'Formulaire d\'enregistrement';
+$lang['Contact form'] = 'Formulaire de contact';
+$lang['Guestbook'] = 'Livre d\'or';
+$lang['Preview'] = 'Prévisualisation';
+$lang['Background'] = 'Fond';
+$lang['Border'] = 'Bordure';
+$lang['Challenge'] = 'Test';
+$lang['Colors'] = 'Couleurs';
+$lang['Drag & drop options'] = 'Options drag & drop';
+$lang['Drop'] = 'Cible';
+$lang['Drop border'] = 'Bordure cible';
+$lang['Image'] = 'Image';
+$lang['Image border'] = 'Bordure image';
+$lang['Image drag & drop'] = 'Drag & drop d\'une image';
+$lang['Image size'] = 'Taille de l\'image';
+$lang['Invalid answer'] = 'Réponse invalide';
+$lang['Marks'] = 'Marques';
+$lang['Number of images'] = 'Nombre d\'images';
+$lang['Pleaser answer'] = 'Veuillez répondre';
+$lang['Random'] = 'Aléatoire';
+$lang['Text'] = 'Texte';
+$lang['Theme'] = 'Thème';
+$lang['Invalid color code <i>%s</i>'] = 'Code couleur invalide <i>%s</i>';
+$lang['Tic-tac-toe minigame'] = 'Minijeu morpion';
+$lang['Tic-tac-toe options'] = 'Options morpion';
+$lang['easycaptcha_drag_%s'] = 'Pour vérifier que vous êtez un humain, veuillez placer le <b>%s</b> dans le cadre de droite.';
+$lang['easycaptcha_tictac'] = 'Vous êtez le joueur X, cliquez dans la bonne case pour former une ligne complète.';
+$lang['easycaptcha_javascript_public'] = 'Vous devez activer JavaScript pour pouvoir ajouter une commentaire, désolé du dérangement.';
+$lang['easycaptcha_javascript_admin'] = 'Ce test requiert que JavaScript soit activé sur le navigateur du visteur. Environ 1% des utilisateurs d\'Internet ont JavaScript désactivé.';
+$lang['easycaptcha_cryptocaptcha'] = 'Nous avons détecté que Crypto Captcha est activé sur votre galerie. Les deux plugins peuvent fonctionner en même temps, mais ous aucun prétexte il ne doivent être activés sur les mêmes pages.';
+?>
Index: /extensions/EasyCaptcha/language/hu_HU/index.php
===================================================================
--- /extensions/EasyCaptcha/language/hu_HU/index.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/hu_HU/index.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
Index: /extensions/EasyCaptcha/language/hu_HU/plugin.lang.php
===================================================================
--- /extensions/EasyCaptcha/language/hu_HU/plugin.lang.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/hu_HU/plugin.lang.php	(revision 24215)
@@ -0,0 +1,10 @@
+<?php
+$lang['Moderate'] = 'Moderálás';
+$lang['Preview'] = 'Előnézet';
+$lang['Activate on'] = 'Hol kapcsoljam be?';
+$lang['Album comments'] = 'Album hozzászólások';
+$lang['Contact form'] = 'Kapcsolati űrlap';
+$lang['Guestbook'] = 'Vendégkönyv';
+$lang['Picture comments'] = 'Kép hozzászólások';
+$lang['Register form'] = 'Regisztrációs űrlap';
+?>
Index: /extensions/EasyCaptcha/language/index.php
===================================================================
--- /extensions/EasyCaptcha/language/index.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/index.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
Index: /extensions/EasyCaptcha/language/it_IT/index.php
===================================================================
--- /extensions/EasyCaptcha/language/it_IT/index.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/it_IT/index.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
Index: /extensions/EasyCaptcha/language/it_IT/plugin.lang.php
===================================================================
--- /extensions/EasyCaptcha/language/it_IT/plugin.lang.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/it_IT/plugin.lang.php	(revision 24215)
@@ -0,0 +1,10 @@
+<?php
+$lang['Moderate'] = 'Moderare';
+$lang['Preview'] = 'Anteprima';
+$lang['Activate on'] = 'Attiva su';
+$lang['Album comments'] = 'Commenti album';
+$lang['Contact form'] = 'Modulo contatti';
+$lang['Guestbook'] = 'Libro ospiti';
+$lang['Picture comments'] = 'Commenti immagini';
+$lang['Register form'] = 'Modulo registrazione';
+?>
Index: /extensions/EasyCaptcha/language/lv_LV/index.php
===================================================================
--- /extensions/EasyCaptcha/language/lv_LV/index.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/lv_LV/index.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
Index: /extensions/EasyCaptcha/language/lv_LV/plugin.lang.php
===================================================================
--- /extensions/EasyCaptcha/language/lv_LV/plugin.lang.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/lv_LV/plugin.lang.php	(revision 24215)
@@ -0,0 +1,10 @@
+<?php
+$lang['Moderate'] = 'Apmierinošs';
+$lang['Preview'] = 'Priekšskatījums';
+$lang['Activate on'] = 'Aktivēt uz';
+$lang['Album comments'] = 'Albūma komentāri';
+$lang['Contact form'] = 'Kontaktforma';
+$lang['Guestbook'] = 'Viesu grāmata';
+$lang['Picture comments'] = 'Attēla komentāri';
+$lang['Register form'] = 'Reģistra forma';
+?>
Index: /extensions/EasyCaptcha/language/nl_NL/index.php
===================================================================
--- /extensions/EasyCaptcha/language/nl_NL/index.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/nl_NL/index.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
Index: /extensions/EasyCaptcha/language/nl_NL/plugin.lang.php
===================================================================
--- /extensions/EasyCaptcha/language/nl_NL/plugin.lang.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/nl_NL/plugin.lang.php	(revision 24215)
@@ -0,0 +1,11 @@
+<?php
+$lang['Activate on'] = 'Activeren';
+$lang['Album comments'] = 'Album commentaar';
+$lang['Guestbook'] = 'Gastenboek';
+$lang['Contact form'] = 'Contact formulier';
+$lang['Moderate'] = 'Matig';
+$lang['Comments action'] = 'Commentaar actie';
+$lang['Register form'] = 'Registratieformulier';
+$lang['Preview'] = 'Voorbeeld';
+$lang['Picture comments'] = 'Afbeelding commentaar';
+?>
Index: /extensions/EasyCaptcha/language/pl_PL/index.php
===================================================================
--- /extensions/EasyCaptcha/language/pl_PL/index.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/pl_PL/index.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
Index: /extensions/EasyCaptcha/language/pl_PL/plugin.lang.php
===================================================================
--- /extensions/EasyCaptcha/language/pl_PL/plugin.lang.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/pl_PL/plugin.lang.php	(revision 24215)
@@ -0,0 +1,10 @@
+<?php
+$lang['Moderate'] = 'Średni';
+$lang['Preview'] = 'Podgląd';
+$lang['Activate on'] = 'Aktywuj dla';
+$lang['Album comments'] = 'Komentarze albumu';
+$lang['Contact form'] = 'Formularz kontaktowy';
+$lang['Guestbook'] = 'Księga gości';
+$lang['Picture comments'] = 'Komentarze zdjęć';
+$lang['Register form'] = 'Formularz rejestracyjny';
+?>
Index: /extensions/EasyCaptcha/language/pt_BR/index.php
===================================================================
--- /extensions/EasyCaptcha/language/pt_BR/index.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/pt_BR/index.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
Index: /extensions/EasyCaptcha/language/pt_BR/plugin.lang.php
===================================================================
--- /extensions/EasyCaptcha/language/pt_BR/plugin.lang.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/pt_BR/plugin.lang.php	(revision 24215)
@@ -0,0 +1,11 @@
+<?php
+$lang['Picture comments'] = 'Comentários de imagens';
+$lang['Preview'] = 'Previsão';
+$lang['Register form'] = 'Formulário de registro';
+$lang['Activate on'] = 'Ativar em';
+$lang['Album comments'] = 'Comentários de album';
+$lang['Comments action'] = 'Ação comentários';
+$lang['Contact form'] = 'Formulário de contato';
+$lang['Guestbook'] = 'Livro de Visitas';
+$lang['Moderate'] = 'Moderada';
+?>
Index: /extensions/EasyCaptcha/language/pt_PT/index.php
===================================================================
--- /extensions/EasyCaptcha/language/pt_PT/index.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/pt_PT/index.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
Index: /extensions/EasyCaptcha/language/pt_PT/plugin.lang.php
===================================================================
--- /extensions/EasyCaptcha/language/pt_PT/plugin.lang.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/pt_PT/plugin.lang.php	(revision 24215)
@@ -0,0 +1,10 @@
+<?php
+$lang['Preview'] = 'Pré-visualizar';
+$lang['Register form'] = 'Formulário de registo';
+$lang['Activate on'] = 'Activar ligado';
+$lang['Album comments'] = 'Comentários aos Álbuns';
+$lang['Comments action'] = 'Acção aos comentários';
+$lang['Contact form'] = 'Formulário de contacto';
+$lang['Guestbook'] = 'Livro de visitas';
+$lang['Moderate'] = 'Moderar';
+?>
Index: /extensions/EasyCaptcha/language/ru_RU/index.php
===================================================================
--- /extensions/EasyCaptcha/language/ru_RU/index.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/ru_RU/index.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
Index: /extensions/EasyCaptcha/language/ru_RU/plugin.lang.php
===================================================================
--- /extensions/EasyCaptcha/language/ru_RU/plugin.lang.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/ru_RU/plugin.lang.php	(revision 24215)
@@ -0,0 +1,11 @@
+<?php
+$lang['Comments action'] = 'Комментарии действий';
+$lang['Moderate'] = 'Проверить';
+$lang['Preview'] = 'Предварительный просмотр';
+$lang['Activate on'] = 'Активировать';
+$lang['Album comments'] = 'Комментарии альбома';
+$lang['Contact form'] = 'Контактная форма';
+$lang['Guestbook'] = 'Гостевая книга';
+$lang['Picture comments'] = 'Комментарии к картинкам';
+$lang['Register form'] = 'Форма регистрации';
+?>
Index: /extensions/EasyCaptcha/language/sk_SK/index.php
===================================================================
--- /extensions/EasyCaptcha/language/sk_SK/index.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/sk_SK/index.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
Index: /extensions/EasyCaptcha/language/sk_SK/plugin.lang.php
===================================================================
--- /extensions/EasyCaptcha/language/sk_SK/plugin.lang.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/sk_SK/plugin.lang.php	(revision 24215)
@@ -0,0 +1,11 @@
+<?php
+$lang['Activate on'] = 'Aktivuje';
+$lang['Album comments'] = 'Komentáre albumu';
+$lang['Comments action'] = 'Akcia s komentármi';
+$lang['Contact form'] = 'Kontaktný formulár';
+$lang['Guestbook'] = 'Kniha hostí';
+$lang['Moderate'] = 'Moderuj';
+$lang['Picture comments'] = 'Komentáre fotky';
+$lang['Preview'] = 'Náhľad';
+$lang['Register form'] = 'Registračný formulár';
+?>
Index: /extensions/EasyCaptcha/language/sv_SE/index.php
===================================================================
--- /extensions/EasyCaptcha/language/sv_SE/index.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/sv_SE/index.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
Index: /extensions/EasyCaptcha/language/sv_SE/plugin.lang.php
===================================================================
--- /extensions/EasyCaptcha/language/sv_SE/plugin.lang.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/sv_SE/plugin.lang.php	(revision 24215)
@@ -0,0 +1,11 @@
+<?php
+$lang['Activate on'] = 'Aktivera på';
+$lang['Comments action'] = 'Kommentarshandling';
+$lang['Register form'] = 'Registreringsformulär';
+$lang['Picture comments'] = 'Bildkommentarer';
+$lang['Preview'] = 'Förhandsgranska';
+$lang['Album comments'] = 'Albumkommentarer';
+$lang['Contact form'] = 'Kontaktformulär';
+$lang['Guestbook'] = 'Gästbok';
+$lang['Moderate'] = 'Moderera';
+?>
Index: /extensions/EasyCaptcha/language/tr_TR/index.php
===================================================================
--- /extensions/EasyCaptcha/language/tr_TR/index.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/tr_TR/index.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
Index: /extensions/EasyCaptcha/language/tr_TR/plugin.lang.php
===================================================================
--- /extensions/EasyCaptcha/language/tr_TR/plugin.lang.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/tr_TR/plugin.lang.php	(revision 24215)
@@ -0,0 +1,11 @@
+<?php
+$lang['Preview'] = 'Ön görünüm';
+$lang['Register form'] = 'Kayıt formu';
+$lang['Activate on'] = 'Etkinleştir';
+$lang['Album comments'] = 'Albüm yorumları';
+$lang['Comments action'] = 'Yorum eylemi';
+$lang['Contact form'] = 'İletişim formu';
+$lang['Guestbook'] = 'Misafir defteri';
+$lang['Moderate'] = 'Orta';
+$lang['Picture comments'] = 'Resim yorumları';
+?>
Index: /extensions/EasyCaptcha/language/uk_UA/index.php
===================================================================
--- /extensions/EasyCaptcha/language/uk_UA/index.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/uk_UA/index.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
Index: /extensions/EasyCaptcha/language/uk_UA/plugin.lang.php
===================================================================
--- /extensions/EasyCaptcha/language/uk_UA/plugin.lang.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/uk_UA/plugin.lang.php	(revision 24215)
@@ -0,0 +1,11 @@
+<?php
+$lang['Comments action'] = 'Коментарі дій';
+$lang['Moderate'] = 'Перевірити';
+$lang['Preview'] = 'Попередній перегляд';
+$lang['Activate on'] = 'Активовано';
+$lang['Album comments'] = 'Коментарі до альбому';
+$lang['Contact form'] = 'Контакт з';
+$lang['Guestbook'] = 'Гостьова книга';
+$lang['Picture comments'] = 'Коментарі до зображення';
+$lang['Register form'] = 'Зареєстровано з';
+?>
Index: /extensions/EasyCaptcha/language/zh_CN/index.php
===================================================================
--- /extensions/EasyCaptcha/language/zh_CN/index.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/zh_CN/index.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
Index: /extensions/EasyCaptcha/language/zh_CN/plugin.lang.php
===================================================================
--- /extensions/EasyCaptcha/language/zh_CN/plugin.lang.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/zh_CN/plugin.lang.php	(revision 24215)
@@ -0,0 +1,9 @@
+<?php
+$lang['Register form'] = '注册界面';
+$lang['Contact form'] = 'ContactForm 插件';
+$lang['Guestbook'] = 'Guestbook 插件';
+$lang['Activate on'] = '在何处启用';
+$lang['Picture comments'] = '图片评论';
+$lang['Preview'] = '预览';
+$lang['Album comments'] = '相册评论';
+?>
Index: /extensions/EasyCaptcha/language/zh_TW/index.php
===================================================================
--- /extensions/EasyCaptcha/language/zh_TW/index.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/zh_TW/index.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
Index: /extensions/EasyCaptcha/language/zh_TW/plugin.lang.php
===================================================================
--- /extensions/EasyCaptcha/language/zh_TW/plugin.lang.php	(revision 24215)
+++ /extensions/EasyCaptcha/language/zh_TW/plugin.lang.php	(revision 24215)
@@ -0,0 +1,11 @@
+<?php
+$lang['Comments action'] = '評論行動';
+$lang['Moderate'] = '中等';
+$lang['Picture comments'] = '圖片評論';
+$lang['Register form'] = '註冊表格';
+$lang['Preview'] = '預覽';
+$lang['Guestbook'] = '訪客留言本';
+$lang['Contact form'] = '聯繫表單';
+$lang['Album comments'] = '相冊評論';
+$lang['Activate on'] = '在何處啟用';
+?>
Index: /extensions/EasyCaptcha/main.inc.php
===================================================================
--- /extensions/EasyCaptcha/main.inc.php	(revision 24215)
+++ /extensions/EasyCaptcha/main.inc.php	(revision 24215)
@@ -0,0 +1,126 @@
+<?php
+/*
+Plugin Name: Easy Captcha
+Version: auto
+Description: An fun antibot system for comments, registration, ContactForm and GuestBook.
+Plugin URI: auto
+Author: Mistic
+Author URI: http://www.strangeplanet.fr
+*/
+
+defined('PHPWG_ROOT_PATH') or die('Hacking attempt!');
+
+// TODO : test on mobile
+if (mobile_theme())
+{
+  return;
+}
+
+defined('EASYCAPTCHA_ID') or define('EASYCAPTCHA_ID', basename(dirname(__FILE__)));
+define('EASYCAPTCHA_PATH' , PHPWG_PLUGINS_PATH . EASYCAPTCHA_ID . '/');
+define('EASYCAPTCHA_ADMIN', get_root_url() . 'admin.php?page=plugin-' . EASYCAPTCHA_ID);
+define('EASYCAPTCHA_VERSION', 'auto');
+
+
+add_event_handler('init', 'easycaptcha_init');
+
+if (defined('IN_ADMIN'))
+{
+  add_event_handler('get_admin_plugin_menu_links', 'easycaptcha_plugin_admin_menu');
+}
+else
+{
+  add_event_handler('init', 'easycaptcha_document_init');
+  add_event_handler('loc_end_section_init', 'easycaptcha_section_init', EVENT_HANDLER_PRIORITY_NEUTRAL+30);
+}
+
+
+// plugin init
+function easycaptcha_init()
+{
+  global $conf, $pwg_loaded_plugins;
+
+  if (
+    EASYCAPTCHA_VERSION == 'auto' or
+    $pwg_loaded_plugins[EASYCAPTCHA_ID]['version'] == 'auto' or
+    version_compare($pwg_loaded_plugins[EASYCAPTCHA_ID]['version'], EASYCAPTCHA_VERSION, '<')
+  )
+  {
+    include_once(EASYCAPTCHA_PATH . 'include/install.inc.php');
+    easycaptcha_install();
+
+    if ( $pwg_loaded_plugins[EASYCAPTCHA_ID]['version'] != 'auto' && EASYCAPTCHA_VERSION != 'auto' )
+    {
+      $query = '
+UPDATE '. PLUGINS_TABLE .'
+SET version = "'. EASYCAPTCHA_VERSION .'"
+WHERE id = "'. EASYCAPTCHA_ID .'"';
+      pwg_query($query);
+
+      $pwg_loaded_plugins[EASYCAPTCHA_ID]['version'] = EASYCAPTCHA_VERSION;
+    }
+  }
+
+  load_language('plugin.lang', EASYCAPTCHA_PATH);
+  $conf['EasyCaptcha'] = unserialize($conf['EasyCaptcha']);
+}
+
+
+// modules : picture comment & register
+function easycaptcha_document_init()
+{
+  global $conf, $user;
+
+  if (!is_a_guest()) return;
+
+  if ( script_basename() == 'register' && $conf['EasyCaptcha']['activate_on']['register'] )
+  {
+    $conf['EasyCaptcha']['template'] = 'register';
+    include(EASYCAPTCHA_PATH . 'include/register.inc.php');
+  }
+  else if ( script_basename() == 'picture' && $conf['EasyCaptcha']['activate_on']['picture'] )
+  {
+    $conf['EasyCaptcha']['template'] = 'comment';
+    include(EASYCAPTCHA_PATH . 'include/picture.inc.php');
+  }
+
+}
+
+// modules : album comment & contact & guestbook
+function easycaptcha_section_init()
+{
+  global $conf, $pwg_loaded_plugins, $page;
+
+  if (!is_a_guest() || !isset($page['section'])) return;
+
+  if (
+    $page['section'] == 'categories' && isset($page['category']) &&
+    isset($pwg_loaded_plugins['Comments_on_Albums']) &&
+    $conf['EasyCaptcha']['activate_on']['category']
+    )
+  {
+    $conf['EasyCaptcha']['template'] = 'comment';
+    include(EASYCAPTCHA_PATH . 'include/category.inc.php');
+  }
+  else if ( $page['section'] == 'contact' && $conf['EasyCaptcha']['activate_on']['contactform'] )
+  {
+    $conf['EasyCaptcha']['template'] = 'contactform';
+    include(EASYCAPTCHA_PATH . 'include/contactform.inc.php');
+  }
+  else if ( $page['section'] == 'guestbook' && $conf['EasyCaptcha']['activate_on']['guestbook'] )
+  {
+    $conf['EasyCaptcha']['template'] = 'guestbook';
+    include(EASYCAPTCHA_PATH . 'include/guestbook.inc.php');
+  }
+}
+
+
+// admin
+function easycaptcha_plugin_admin_menu($menu)
+{
+  array_push($menu, array(
+    'NAME' => 'Easy Captcha',
+    'URL' => EASYCAPTCHA_ADMIN,
+    ));
+  return $menu;
+}
Index: /extensions/EasyCaptcha/maintain.inc.php
===================================================================
--- /extensions/EasyCaptcha/maintain.inc.php	(revision 24215)
+++ /extensions/EasyCaptcha/maintain.inc.php	(revision 24215)
@@ -0,0 +1,24 @@
+<?php
+defined('PHPWG_ROOT_PATH') or die('Hacking attempt!');
+
+defined('EASYCAPTCHA_ID') or define('EASYCAPTCHA_ID', basename(dirname(__FILE__)));
+include_once(PHPWG_PLUGINS_PATH . EASYCAPTCHA_ID . '/include/install.inc.php');
+
+function plugin_install()
+{
+  easycaptcha_install();
+  define('easycaptcha_installed', true);
+}
+
+function plugin_activate()
+{
+  if (!defined('easycaptcha_installed'))
+  {
+    easycaptcha_install();
+  }
+}
+
+function plugin_uninstall()
+{
+  pwg_query('DELETE FROM '.CONFIG_TABLE.' WHERE param="EasyCaptcha" LIMIT 1');
+}
Index: /extensions/EasyCaptcha/template/admin.tpl
===================================================================
--- /extensions/EasyCaptcha/template/admin.tpl	(revision 24215)
+++ /extensions/EasyCaptcha/template/admin.tpl	(revision 24215)
@@ -0,0 +1,303 @@
+{combine_css path=$EASYCAPTCHA_PATH|@cat:"template/style.css"}
+
+{combine_css path="themes/default/js/plugins/chosen.css"}
+{combine_script id='jquery.chosen' load='footer' path='themes/default/js/plugins/chosen.jquery.min.js'}
+
+{combine_css path=$EASYCAPTCHA_PATH|cat:'template/bgrins-spectrum/spectrum.css'}
+{combine_script id='jquery.spectrum' load='footer' path=$EASYCAPTCHA_PATH|cat:'template/bgrins-spectrum/spectrum.js'}
+
+{assign var="spectrum_language" value=$EASYCAPTCHA_PATH|cat:'template/bgrins-spectrum/i18n/jquery.spectrum-'|cat:$lang_info.code|cat:'.js'}
+{if 'PHPWG_ROOT_PATH'|@constant|@cat:$spectrum_language|file_exists}
+{combine_script id='jquery.spectrum.'|cat:$lang_info.code load='footer' require='jquery.spectrum' path=$spectrum_language}
+{/if}
+
+
+{footer_script}{literal}
+// multiselect
+$("select").css({
+    "width":"700px"
+}).chosen({
+    disable_search:true,
+});
+
+// Spectrum settings
+$.extend($.fn.spectrum.defaults, {
+    showInput: true,
+    showPalette: true,
+    localStorageKey: "spectrum.easycaptcha",
+    showInitial: true,
+    preferredFormat: "hex6",
+});
+
+// Tic-tac-toe preview
+$('.preview-tictac').on('change', function() {
+    var inputs = ['size','bg1','bg2','bd','obj','sel'],
+        url = '{/literal}{$EASYCAPTCHA_PATH}{literal}' + 'tictac/gen_admin.php?t='+ new Date().getTime();
+
+    for (var i=0; i<inputs.length; i++) {
+        url+= '&'+ inputs[i] +'='+ encodeURIComponent($('input[name="tictac['+ inputs[i] +']"]').val());
+    }
+
+    $('#preview-tictac').attr('src', url);
+});
+$('.preview-tictac').eq(0).trigger('change');
+
+// Drag & drop preview
+{/literal}
+var themes = {
+{foreach from=$THEMES item=params key=theme}
+  '{$theme}': '{$EASYCAPTCHA_PATH}drag/{$theme}/{$params.image}',
+{/foreach}
+};
+{literal}
+
+$('.preview-drag').on('change', function() {
+    // style
+    var inputs = ['size','nb','bg1','bg2','obj','sel','bd1','bd2','txt'],
+        style = $('#drag_style_src').text(),
+        search, replace;
+
+    for (var i=0; i<inputs.length; i++) {
+        search = '{\\$EASYCAPTCHA_CONF\\.'+ inputs[i] +'}';
+        replace = $('input[name="drag['+ inputs[i] +']"]').val();
+        style = style.replace(new RegExp(search, 'g'), replace);
+    }
+
+    var x = parseInt($('input[name="drag[size]"]').val()),
+        y = parseInt($('input[name="drag[nb]"]').val());
+
+    search = '{dat_equation}',
+    replace = 15+(x+5)*y;
+    style = style.replace(new RegExp(search, 'g'), replace);
+
+    $('#drag_style').text(style);
+
+    // content
+    $('#easycaptcha .drag_item').remove();
+    var html = '',
+        image = themes[$('input[name="drag[theme]"]').val()];
+    for (var i=0; i<y; i++) {
+        html+=
+        '<div class="drag_item" style="left:'+ (10+(x+5)*i) +'px;">'+
+          '<img src="'+ image +'">'+
+        '</div>';
+    }
+    $('#easycaptcha').prepend(html);
+});
+$('#drag_style').appendTo('head'); // move to last position to have priority
+$('.preview-drag').eq(0).trigger('change');
+
+$('#easycaptcha .drop_zone').on({
+    'mouseenter': function() { $(this).addClass('valid'); },
+    'mouseleave': function() { $(this).removeClass('valid'); },
+});
+
+// Drag & drop theme
+$('.theme').on('click', function() {
+  $('.theme').removeClass('selected');
+  $(this).addClass('selected');
+  $('input[name="drag[theme]"]').val($(this).attr('title')).trigger('change');
+});
+$('.theme .title span').css('background-color', $('#content').css('background-color'));
+$('.theme .count span').css('background-color', $('#content').css('background-color'));
+
+// show previews
+$('.preview').prevAll('a').on('click', function() {
+    $(this).hide();
+    $(this).nextAll('.preview').slideDown();
+});
+{/literal}{/footer_script}
+
+
+<div class="titrePage">
+  <h2>Easy Captcha</h2>
+</div>
+
+<form method="post" action="" class="properties">
+<fieldset>
+  <legend>{'Configuration'|translate}</legend>
+
+  <ul>
+    <li>
+      <b>{'Activate on'|translate}</b>
+      <select name="activate_on[]" multiple>
+        <option value="picture" {if $easycaptcha.activate_on.picture}selected{/if}>{'Picture comments'|translate}</option>
+        {if $loaded.category}<option value="category" {if $easycaptcha.activate_on.category}selected{/if}>{'Album comments'|translate}</option>{/if}
+        <option value="register" {if $easycaptcha.activate_on.register}selected{/if}>{'Register form'|translate}</option>
+        {if $loaded.contactform}<option value="contactform" {if $easycaptcha.activate_on.contactform}selected{/if}>{'Contact form'|translate}</option>{/if}
+        {if $loaded.guestbook}<option value="guestbook" {if $easycaptcha.activate_on.guestbook}selected{/if}>{'Guestbook'|translate}</option>{/if}
+      </select>
+    </li>
+    <li>
+      <b>{'Comments action'|translate}</b>
+      <label><input type="radio" name="comments_action" value="reject" {if $easycaptcha.comments_action == 'reject'}checked="checked"{/if}> {'Reject'|translate}</label>
+      <label><input type="radio" name="comments_action" value="moderate" {if $easycaptcha.comments_action == 'moderate'}checked="checked"{/if}> {'Moderate'|translate}</label>
+    </li>
+    <li>
+      <b>{'Challenge'|translate}</b>
+      <label><input type="radio" name="challenge" value="tictac" {if $easycaptcha.challenge == 'tictac'}checked="checked"{/if}> {'Tic-tac-toe minigame'|translate}</label>
+      <label><input type="radio" name="challenge" value="drag" {if $easycaptcha.challenge == 'drag'}checked="checked"{/if}> {'Image drag & drop'|translate}</label>
+      <label><input type="radio" name="challenge" value="random" {if $easycaptcha.challenge == 'random'}checked="checked"{/if}> {'Random'|translate}</label>
+    </li>
+  </ul>
+
+  <fieldset>
+    <legend>{'Tic-tac-toe options'|translate}</legend>
+
+    <ul>
+      <li>
+        <b>{'Image size'|translate}</b>
+        <input type="number" name="tictac[size]" value="{$easycaptcha.tictac.size}" min=32 max=256 class="preview-tictac">
+      </li>
+      <li>
+        <b>{'Colors'|translate}</b>
+        <table class="colors">
+          <tr>
+            <td>{'Background'|translate} 1</td>
+            <td>{'Background'|translate} 2</td>
+            <td>{'Border'|translate}</td>
+            <td>{'Marks'|translate}</td>
+            <td>{'Selection'|translate}</td>
+          </tr>
+          <tr>
+            <td><input type="color" name="tictac[bg1]" value="{$easycaptcha.tictac.bg1}" class="preview-tictac" size="7"></td>
+            <td><input type="color" name="tictac[bg2]" value="{$easycaptcha.tictac.bg2}" class="preview-tictac" size="7"></td>
+            <td><input type="color" name="tictac[bd]" value="{$easycaptcha.tictac.bd}" class="preview-tictac" size="7"></td>
+            <td><input type="color" name="tictac[obj]" value="{$easycaptcha.tictac.obj}" class="preview-tictac" size="7"></td>
+            <td><input type="color" name="tictac[sel]" value="{$easycaptcha.tictac.sel}" class="preview-tictac" size="7"></td>
+          </tr>
+        </table>
+      </li>
+      <li>
+        <b>&nbsp;</b>
+        <a class="buttonLike">{'Preview'|translate}</a>
+        <div class="preview">
+          <img id="preview-tictac" src="">
+        </div>
+      </li>
+    </ul>
+  </fieldset>
+
+  <fieldset>
+    <legend>{'Drag & drop options'|translate}</legend>
+
+    <div class="warnings my-warnings">
+      <ul><li>{'easycaptcha_javascript_admin'|translate}</li></ul>
+    </div>
+
+    <ul>
+      <li>
+        <b>{'Theme'|translate}</b>
+        {foreach from=$THEMES key=theme item=params}
+        <a class="theme {if $easycaptcha.drag.theme == $theme}selected{/if}" title="{$theme}">
+          <div class="title"><span>{$theme|ucfirst}</span></div>
+          <div class="count"><span>({$params.count})</span></div>
+          <img src="{$EASYCAPTCHA_PATH}drag/{$theme}/{$params.image}">
+        </a>
+        {/foreach}
+        <input type="hidden" name="drag[theme]" value="{$easycaptcha.drag.theme}" class="preview-drag">
+      </li>
+      <li>
+        <b>{'Image size'|translate}</b>
+        <label><input type="number" name="drag[size]" value="{$easycaptcha.drag.size}" min=24 max=128 class="preview-drag"></label>
+      </li>
+      <li>
+        <b>{'Number of images'|translate}</b>
+        <label><input type="number" name="drag[nb]" value="{$easycaptcha.drag.nb}" min=3 max=10 class="preview-drag"></label>
+      </li>
+      <li>
+        <b>{'Colors'|translate}</b>
+        <table class="colors">
+          <tr>
+            <td>{'Background'|translate} 1</td>
+            <td>{'Background'|translate} 2</td>
+            <td>{'Image'|translate}</td>
+            <td>{'Image border'|translate}</td>
+            <td>{'Drop'|translate}</td>
+            <td>{'Drop border'|translate}</td>
+            <td>{'Text'|translate}</td>
+          </tr>
+          <tr>
+            <td><input type="color" name="drag[bg1]" value="{$easycaptcha.drag.bg1}" class="preview-drag" size="7"></td>
+            <td><input type="color" name="drag[bg2]" value="{$easycaptcha.drag.bg2}" class="preview-drag" size="7"></td>
+            <td><input type="color" name="drag[obj]" value="{$easycaptcha.drag.obj}" class="preview-drag" size="7"></td>
+            <td><input type="color" name="drag[bd1]" value="{$easycaptcha.drag.bd1}" class="preview-drag" size="7"></td>
+            <td><input type="color" name="drag[sel]" value="{$easycaptcha.drag.sel}" class="preview-drag" size="7"></td>
+            <td><input type="color" name="drag[bd2]" value="{$easycaptcha.drag.bd2}" class="preview-drag" size="7"></td>
+            <td><input type="color" name="drag[txt]" value="{$easycaptcha.drag.txt}" class="preview-drag" size="7"></td>
+          </tr>
+        </table>
+      </li>
+      <li>
+        <b>&nbsp;</b>
+        <a class="buttonLike">{'Preview'|translate}</a>
+        <div class="preview">
+          {include file=$EASYCAPTCHA_ABS_PATH|cat:'template/common.inc.tpl' EASYCAPTCHA_CHALLENGE='drag' EASYCAPTCHA_CONF=$easycaptcha.drag}
+          {$smarty.capture.easycaptcha}
+        </div>
+      </li>
+    </ul>
+  </fieldset>
+</fieldset>
+
+<p class="formButtons"><input class="submit" type="submit" value="{'Submit'|translate}" name="submit"></p>
+</form>
+
+<div style="text-align:right;">
+  Icons
+    [<a href="https://www.iconfinder.com/iconsets/cutecritters" class="externalLink">#1</a>]
+    [<a href="https://www.iconfinder.com/iconsets/crystalproject" class="externalLink">#2</a>]
+    [<a href="https://www.iconfinder.com/iconsets/UrbanStories-png-Artdesigner-lv" class="externalLink">#3</a>]
+    [<a href="https://www.iconfinder.com/iconsets/ie_Bright" class="externalLink">#4</a>]
+  | Libraries
+    [<a href="http://bgrins.github.io/spectrum"class="externalLink">Spectrum.js</a>]
+    [<a href="http://threedubmedia.com"class="externalLink">jQuery.events</a>]
+</div>
+
+
+{* <!-- weird thing to update bunch of CSS --> *}
+{html_head}{literal}
+<style id="drag_style"></style>
+<script type="text/template" id="drag_style_src">
+#easycaptcha, #easycaptcha_noscript {
+  background: {$EASYCAPTCHA_CONF.bg1};
+  background: -webkit-linear-gradient(top, {$EASYCAPTCHA_CONF.bg1} 0%, {$EASYCAPTCHA_CONF.bg2} 100%);
+  background: -moz-linear-gradient(top, {$EASYCAPTCHA_CONF.bg1} 0%, {$EASYCAPTCHA_CONF.bg2} 100%);
+  background: -ms-linear-gradient(top, {$EASYCAPTCHA_CONF.bg1} 0%, {$EASYCAPTCHA_CONF.bg2} 100%);
+  background: -o-linear-gradient(top, {$EASYCAPTCHA_CONF.bg1} 0%, {$EASYCAPTCHA_CONF.bg2} 100%);
+  background: linear-gradient(to bottom, {$EASYCAPTCHA_CONF.bg1} 0%, {$EASYCAPTCHA_CONF.bg2} 100%);
+  color:{$EASYCAPTCHA_CONF.txt};
+}
+#easycaptcha .drag_item {
+  width:{$EASYCAPTCHA_CONF.size}px;
+  height:{$EASYCAPTCHA_CONF.size}px;
+  border:1px solid {$EASYCAPTCHA_CONF.bd1};
+  background:{$EASYCAPTCHA_CONF.obj};
+}
+#easycaptcha .drag_item img {
+  width:{$EASYCAPTCHA_CONF.size}px;
+  height:{$EASYCAPTCHA_CONF.size}px;
+}
+#easycaptcha .drop_zone {
+  width:{$EASYCAPTCHA_CONF.size}px;
+  height:{$EASYCAPTCHA_CONF.size}px;
+  margin-left:{dat_equation}px;
+  line-height:{$EASYCAPTCHA_CONF.size}px;
+  background: {$EASYCAPTCHA_CONF.bg1};
+  background: -webkit-linear-gradient(bottom, {$EASYCAPTCHA_CONF.bg1} 0%, {$EASYCAPTCHA_CONF.bg2} 100%);
+  background: -moz-linear-gradient(bottom, {$EASYCAPTCHA_CONF.bg1} 0%, {$EASYCAPTCHA_CONF.bg2} 100%);
+  background: -ms-linear-gradient(bottom, {$EASYCAPTCHA_CONF.bg1} 0%, {$EASYCAPTCHA_CONF.bg2} 100%);
+  background: -o-linear-gradient(bottom, {$EASYCAPTCHA_CONF.bg1} 0%, {$EASYCAPTCHA_CONF.bg2} 100%);
+  background: linear-gradient(to top, {$EASYCAPTCHA_CONF.bg1} 0%, {$EASYCAPTCHA_CONF.bg2} 100%);
+  border:1px dotted {$EASYCAPTCHA_CONF.bd2};
+  color:{$EASYCAPTCHA_CONF.txt};
+}
+#easycaptcha .drop_zone.active {
+  background:{$EASYCAPTCHA_CONF.sel};
+}
+#easycaptcha .drop_zone.valid {
+  background:{$EASYCAPTCHA_CONF.sel};
+  box-shadow:0 0 0 2px {$EASYCAPTCHA_CONF.sel};
+}
+</script>
+{/literal}{/html_head}
Index: /extensions/EasyCaptcha/template/bgrins-spectrum/i18n/jquery.spectrum-de.js
===================================================================
--- /extensions/EasyCaptcha/template/bgrins-spectrum/i18n/jquery.spectrum-de.js	(revision 24215)
+++ /extensions/EasyCaptcha/template/bgrins-spectrum/i18n/jquery.spectrum-de.js	(revision 24215)
@@ -0,0 +1,14 @@
+// Spectrum Colorpicker
+// German (de) localization
+// https://github.com/bgrins/spectrum
+
+(function ( $ ) {
+
+    var localization = $.spectrum.localization["de"] = {
+        cancelText: "Abbrechen",
+        chooseText: "Wählen"
+    };
+
+    $.extend($.fn.spectrum.defaults, localization);
+
+})( jQuery );
Index: /extensions/EasyCaptcha/template/bgrins-spectrum/i18n/jquery.spectrum-dk.js
===================================================================
--- /extensions/EasyCaptcha/template/bgrins-spectrum/i18n/jquery.spectrum-dk.js	(revision 24215)
+++ /extensions/EasyCaptcha/template/bgrins-spectrum/i18n/jquery.spectrum-dk.js	(revision 24215)
@@ -0,0 +1,14 @@
+// Spectrum Colorpicker
+// Danish (dk) localization
+// https://github.com/bgrins/spectrum
+
+(function ( $ ) {
+
+    var localization = $.spectrum.localization["dk"] = {
+		cancelText: "annuller",
+		chooseText: "Vælg"
+    };
+
+    $.extend($.fn.spectrum.defaults, localization);
+
+})( jQuery );
Index: /extensions/EasyCaptcha/template/bgrins-spectrum/i18n/jquery.spectrum-es.js
===================================================================
--- /extensions/EasyCaptcha/template/bgrins-spectrum/i18n/jquery.spectrum-es.js	(revision 24215)
+++ /extensions/EasyCaptcha/template/bgrins-spectrum/i18n/jquery.spectrum-es.js	(revision 24215)
@@ -0,0 +1,14 @@
+// Spectrum Colorpicker
+// Spanish (es) localization
+// https://github.com/bgrins/spectrum
+
+(function ( $ ) {
+
+    var localization = $.spectrum.localization["es"] = {
+        cancelText: "Cancelar",
+        chooseText: "Elegir"
+    };
+
+    $.extend($.fn.spectrum.defaults, localization);
+
+})( jQuery );
Index: /extensions/EasyCaptcha/template/bgrins-spectrum/i18n/jquery.spectrum-fi.js
===================================================================
--- /extensions/EasyCaptcha/template/bgrins-spectrum/i18n/jquery.spectrum-fi.js	(revision 24215)
+++ /extensions/EasyCaptcha/template/bgrins-spectrum/i18n/jquery.spectrum-fi.js	(revision 24215)
@@ -0,0 +1,14 @@
+// Spectrum Colorpicker
+// Finnish (fi) localization
+// https://github.com/bgrins/spectrum
+
+(function ( $ ) {
+
+    var localization = $.spectrum.localization["fi"] = {
+        cancelText: "Kumoa",
+        chooseText: "Valitse"
+    };
+
+    $.extend($.fn.spectrum.defaults, localization);
+
+})( jQuery );
Index: /extensions/EasyCaptcha/template/bgrins-spectrum/i18n/jquery.spectrum-fr.js
===================================================================
--- /extensions/EasyCaptcha/template/bgrins-spectrum/i18n/jquery.spectrum-fr.js	(revision 24215)
+++ /extensions/EasyCaptcha/template/bgrins-spectrum/i18n/jquery.spectrum-fr.js	(revision 24215)
@@ -0,0 +1,14 @@
+// Spectrum Colorpicker
+// French (fr) localization
+// https://github.com/bgrins/spectrum
+
+(function ( $ ) {
+
+    var localization = $.spectrum.localization["fr"] = {
+        cancelText: "Annuler",
+        chooseText: "Valider"
+    };
+
+    $.extend($.fn.spectrum.defaults, localization);
+
+})( jQuery );
Index: /extensions/EasyCaptcha/template/bgrins-spectrum/i18n/jquery.spectrum-it.js
===================================================================
--- /extensions/EasyCaptcha/template/bgrins-spectrum/i18n/jquery.spectrum-it.js	(revision 24215)
+++ /extensions/EasyCaptcha/template/bgrins-spectrum/i18n/jquery.spectrum-it.js	(revision 24215)
@@ -0,0 +1,14 @@
+// Spectrum Colorpicker
+// Italian (it) localization
+// https://github.com/bgrins/spectrum
+
+(function ( $ ) {
+
+    var localization = $.spectrum.localization["it"] = {
+		cancelText: "annulla",
+		chooseText: "scegli"
+	};
+
+    $.extend($.fn.spectrum.defaults, localization);
+
+})( jQuery );
Index: /extensions/EasyCaptcha/template/bgrins-spectrum/i18n/jquery.spectrum-ja.js
===================================================================
--- /extensions/EasyCaptcha/template/bgrins-spectrum/i18n/jquery.spectrum-ja.js	(revision 24215)
+++ /extensions/EasyCaptcha/template/bgrins-spectrum/i18n/jquery.spectrum-ja.js	(revision 24215)
@@ -0,0 +1,14 @@
+// Spectrum Colorpicker
+// Japanese (ja) localization
+// https://github.com/bgrins/spectrum
+
+(function ( $ ) {
+
+    var localization = $.spectrum.localization["ja"] = {
+        cancelText: "中止",
+        chooseText: "選択"
+    };
+
+    $.extend($.fn.spectrum.defaults, localization);
+
+})( jQuery );
Index: /extensions/EasyCaptcha/template/bgrins-spectrum/i18n/jquery.spectrum-ru.js
===================================================================
--- /extensions/EasyCaptcha/template/bgrins-spectrum/i18n/jquery.spectrum-ru.js	(revision 24215)
+++ /extensions/EasyCaptcha/template/bgrins-spectrum/i18n/jquery.spectrum-ru.js	(revision 24215)
@@ -0,0 +1,14 @@
+// Spectrum Colorpicker
+// Russian (ru) localization
+// https://github.com/bgrins/spectrum
+
+(function ( $ ) {
+
+    var localization = $.spectrum.localization["ru"] = {
+		cancelText: "отмена",
+		chooseText: "выбрать"
+	};
+
+    $.extend($.fn.spectrum.defaults, localization);
+
+})( jQuery );
Index: /extensions/EasyCaptcha/template/bgrins-spectrum/i18n/jquery.spectrum-sv.js
===================================================================
--- /extensions/EasyCaptcha/template/bgrins-spectrum/i18n/jquery.spectrum-sv.js	(revision 24215)
+++ /extensions/EasyCaptcha/template/bgrins-spectrum/i18n/jquery.spectrum-sv.js	(revision 24215)
@@ -0,0 +1,14 @@
+// Spectrum Colorpicker
+// Swedish (sv) localization
+// https://github.com/bgrins/spectrum
+
+(function ( $ ) {
+
+    var localization = $.spectrum.localization["sv"] = {
+        cancelText: "Avbryt",
+        chooseText: "Välj"
+    };
+
+    $.extend($.fn.spectrum.defaults, localization);
+
+})( jQuery );
Index: /extensions/EasyCaptcha/template/bgrins-spectrum/spectrum.css
===================================================================
--- /extensions/EasyCaptcha/template/bgrins-spectrum/spectrum.css	(revision 24215)
+++ /extensions/EasyCaptcha/template/bgrins-spectrum/spectrum.css	(revision 24215)
@@ -0,0 +1,479 @@
+/***
+Spectrum Colorpicker v1.1.1
+https://github.com/bgrins/spectrum
+Author: Brian Grinstead
+License: MIT
+***/
+
+.sp-container {
+    position:absolute;
+    top:0;
+    left:0;
+    display:inline-block;
+    *display: inline;
+    *zoom: 1;
+    /* https://github.com/bgrins/spectrum/issues/40 */
+    z-index: 9999994;
+    overflow: hidden;
+}
+.sp-container.sp-flat {
+    position: relative;
+}
+
+/* http://ansciath.tumblr.com/post/7347495869/css-aspect-ratio */
+.sp-top {
+  position:relative;
+  width: 100%;
+  display:inline-block;
+}
+.sp-top-inner {
+   position:absolute;
+   top:0;
+   left:0;
+   bottom:0;
+   right:0;
+}
+.sp-color {
+    position: absolute;
+    top:0;
+    left:0;
+    bottom:0;
+    right:20%;
+}
+.sp-hue {
+    position: absolute;
+    top:0;
+    right:0;
+    bottom:0;
+    left:84%;
+    height: 100%;
+}
+.sp-fill {
+    padding-top: 80%;
+}
+.sp-sat, .sp-val {
+    position: absolute;
+    top:0;
+    left:0;
+    right:0;
+    bottom:0;
+}
+
+.sp-alpha-enabled .sp-top {
+    margin-bottom: 18px;
+}
+.sp-alpha-enabled .sp-alpha {
+    display: block;
+}
+.sp-alpha-handle {
+    position:absolute;
+    top:-4px;
+    bottom: -4px;
+    width: 6px;
+    left: 50%;
+    cursor: pointer;
+    border: 1px solid black;
+    background: white;
+    opacity: .8;
+}
+.sp-alpha {
+    display: none;
+    position: absolute;
+    bottom: -14px;
+    right: 0;
+    left: 0;
+    height: 8px;
+}
+.sp-alpha-inner {
+    border: solid 1px #333;
+}
+
+/* Don't allow text selection */
+.sp-container, .sp-replacer, .sp-preview, .sp-dragger, .sp-slider, .sp-alpha, .sp-alpha-handle, .sp-container.sp-dragging .sp-input, .sp-container button  {
+    -webkit-user-select:none;
+    -moz-user-select: -moz-none;
+    -o-user-select:none;
+    user-select: none;
+}
+
+.sp-container.sp-input-disabled .sp-input-container {
+    display: none;
+}
+.sp-container.sp-buttons-disabled .sp-button-container {
+    display: none;
+}
+.sp-palette-only .sp-picker-container {
+    display: none;
+}
+.sp-palette-disabled .sp-palette-container {
+    display: none;
+}
+
+.sp-initial-disabled .sp-initial {
+    display: none;
+}
+
+
+/* Gradients for hue, saturation and value instead of images.  Not pretty... but it works */
+.sp-sat {
+    background-image: -webkit-gradient(linear,  0 0, 100% 0, from(#FFF), to(rgba(204, 154, 129, 0)));
+    background-image: -webkit-linear-gradient(left, #FFF, rgba(204, 154, 129, 0));
+    background-image: -moz-linear-gradient(left, #fff, rgba(204, 154, 129, 0));
+    background-image: -o-linear-gradient(left, #fff, rgba(204, 154, 129, 0));
+    background-image: -ms-linear-gradient(left, #fff, rgba(204, 154, 129, 0));
+    background-image: linear-gradient(to right, #fff, rgba(204, 154, 129, 0));
+    -ms-filter: "progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr=#FFFFFFFF, endColorstr=#00CC9A81)";
+    filter : progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr='#FFFFFFFF', endColorstr='#00CC9A81');
+}
+.sp-val {
+    background-image: -webkit-gradient(linear, 0 100%, 0 0, from(#000000), to(rgba(204, 154, 129, 0)));
+    background-image: -webkit-linear-gradient(bottom, #000000, rgba(204, 154, 129, 0));
+    background-image: -moz-linear-gradient(bottom, #000, rgba(204, 154, 129, 0));
+    background-image: -o-linear-gradient(bottom, #000, rgba(204, 154, 129, 0));
+    background-image: -ms-linear-gradient(bottom, #000, rgba(204, 154, 129, 0));
+    background-image: linear-gradient(to top, #000, rgba(204, 154, 129, 0));
+    -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#00CC9A81, endColorstr=#FF000000)";
+    filter : progid:DXImageTransform.Microsoft.gradient(startColorstr='#00CC9A81', endColorstr='#FF000000');
+}
+
+.sp-hue {
+    background: -moz-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
+    background: -ms-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
+    background: -o-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
+    background: -webkit-gradient(linear, left top, left bottom, from(#ff0000), color-stop(0.17, #ffff00), color-stop(0.33, #00ff00), color-stop(0.5, #00ffff), color-stop(0.67, #0000ff), color-stop(0.83, #ff00ff), to(#ff0000));
+    background: -webkit-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
+}
+
+/* IE filters do not support multiple color stops.
+   Generate 6 divs, line them up, and do two color gradients for each.
+   Yes, really.
+ */
+.sp-1 {
+    height:17%;
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0000', endColorstr='#ffff00');
+}
+.sp-2 {
+    height:16%;
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffff00', endColorstr='#00ff00');
+}
+.sp-3 {
+    height:17%;
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ff00', endColorstr='#00ffff');
+}
+.sp-4 {
+    height:17%;
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ffff', endColorstr='#0000ff');
+}
+.sp-5 {
+    height:16%;
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0000ff', endColorstr='#ff00ff');
+}
+.sp-6 {
+    height:17%;
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff00ff', endColorstr='#ff0000');
+}
+
+.sp-hidden {
+    display: none !important;
+}
+
+/* Clearfix hack */
+.sp-cf:before, .sp-cf:after { content: ""; display: table; }
+.sp-cf:after { clear: both; }
+.sp-cf { *zoom: 1; }
+
+/* Mobile devices, make hue slider bigger so it is easier to slide */
+@media (max-device-width: 480px) {
+    .sp-color { right: 40%; }
+    .sp-hue { left: 63%; }
+    .sp-fill { padding-top: 60%; }
+}
+.sp-dragger {
+   border-radius: 5px;
+   height: 5px;
+   width: 5px;
+   border: 1px solid #fff;
+   background: #000;
+   cursor: pointer;
+   position:absolute;
+   top:0;
+   left: 0;
+}
+.sp-slider {
+    position: absolute;
+    top:0;
+    cursor:pointer;
+    height: 3px;
+    left: -1px;
+    right: -1px;
+    border: 1px solid #000;
+    background: white;
+    opacity: .8;
+}
+
+/*
+Theme authors:
+Here are the basic themeable display options (colors, fonts, global widths).
+See http://bgrins.github.io/spectrum/themes/ for instructions.
+*/
+
+.sp-container {
+    border-radius: 0;
+    background-color: #ECECEC;
+    border: solid 1px #f0c49B;
+    padding: 0;
+}
+.sp-container, .sp-container button, .sp-container input, .sp-color, .sp-hue
+{
+    font: normal 12px "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif;
+    -webkit-box-sizing: border-box;
+    -moz-box-sizing: border-box;
+    -ms-box-sizing: border-box;
+    box-sizing: border-box;
+}
+.sp-top
+{
+    margin-bottom: 3px;
+}
+.sp-color, .sp-hue
+{
+    border: solid 1px #666;
+}
+
+/* Input */
+.sp-input-container {
+    float:right;
+    width: 100px;
+    margin-bottom: 4px;
+}
+.sp-initial-disabled  .sp-input-container {
+    width: 100%;
+}
+.sp-input {
+   font-size: 12px !important;
+   border: 1px inset;
+   padding: 4px 5px;
+   margin: 0;
+   width: 100%;
+   background:transparent;
+   border-radius: 3px;
+   color: #222;
+}
+.sp-input:focus  {
+    border: 1px solid orange;
+}
+.sp-input.sp-validation-error
+{
+    border: 1px solid red;
+    background: #fdd;
+}
+.sp-picker-container , .sp-palette-container
+{
+    float:left;
+    position: relative;
+    padding: 10px;
+    padding-bottom: 300px;
+    margin-bottom: -290px;
+}
+.sp-picker-container
+{
+    width: 172px;
+    border-left: solid 1px #fff;
+}
+
+/* Palettes */
+.sp-palette-container
+{
+    border-right: solid 1px #ccc;
+}
+
+.sp-palette .sp-thumb-el {
+    display: block;
+    position:relative;
+    float:left;
+    width: 24px;
+    height: 15px;
+    margin: 3px;
+    cursor: pointer;
+    border:solid 2px transparent;
+}
+.sp-palette .sp-thumb-el:hover, .sp-palette .sp-thumb-el.sp-thumb-active {
+    border-color: orange;
+}
+.sp-thumb-el
+{
+    position:relative;
+}
+
+/* Initial */
+.sp-initial
+{
+    float: left;
+    border: solid 1px #333;
+}
+.sp-initial span {
+    width: 30px;
+    height: 25px;
+    border:none;
+    display:block;
+    float:left;
+    margin:0;
+}
+
+/* Buttons */
+.sp-button-container {
+    float: right;
+}
+
+/* Replacer (the little preview div that shows up instead of the <input>) */
+.sp-replacer {
+    margin:0;
+    overflow:hidden;
+    cursor:pointer;
+    padding: 4px;
+    display:inline-block;
+    *zoom: 1;
+    *display: inline;
+    border: solid 1px #91765d;
+    background: #eee;
+    color: #333;
+    vertical-align: middle;
+}
+.sp-replacer:hover, .sp-replacer.sp-active {
+    border-color: #F0C49B;
+    color: #111;
+}
+.sp-replacer.sp-disabled {
+    cursor:default;
+    border-color: silver;
+    color: silver;
+}
+.sp-dd {
+    padding: 2px 0;
+    height: 16px;
+    line-height: 16px;
+    float:left;
+    font-size:10px;
+}
+.sp-preview
+{
+    position:relative;
+    width:25px;
+    height: 20px;
+    border: solid 1px #222;
+    margin-right: 5px;
+    float:left;
+    z-index: 0;
+}
+
+.sp-palette
+{
+    *width: 220px;
+    max-width: 220px;
+}
+.sp-palette .sp-thumb-el
+{
+    width:16px;
+    height: 16px;
+    margin:2px 1px;
+    border: solid 1px #d0d0d0;
+}
+
+.sp-container
+{
+    padding-bottom:0;
+}
+
+
+/* Buttons: http://hellohappy.org/css3-buttons/ */
+.sp-container button {
+  background-color: #eeeeee;
+  background-image: -webkit-linear-gradient(top, #eeeeee, #cccccc);
+  background-image: -moz-linear-gradient(top, #eeeeee, #cccccc);
+  background-image: -ms-linear-gradient(top, #eeeeee, #cccccc);
+  background-image: -o-linear-gradient(top, #eeeeee, #cccccc);
+  background-image: -ms-linear-gradient(top, #eeeeee, #cccccc);
+  background-image: linear-gradient(to bottom, #eeeeee, #cccccc);
+  border: 1px solid #ccc;
+  border-bottom: 1px solid #bbb;
+  border-radius: 3px;
+  color: #333;
+  font-size: 14px;
+  line-height: 1;
+  padding: 5px 4px;
+  text-align: center;
+  text-shadow: 0 1px 0 #eee;
+  vertical-align: middle;
+}
+.sp-container button:hover {
+    background-color: #dddddd;
+    background-image: -webkit-linear-gradient(top, #dddddd, #bbbbbb);
+    background-image: -moz-linear-gradient(top, #dddddd, #bbbbbb);
+    background-image: -ms-linear-gradient(top, #dddddd, #bbbbbb);
+    background-image: -o-linear-gradient(top, #dddddd, #bbbbbb);
+    background-image: -ms-linear-gradient(top, #dddddd, #bbbbbb);
+    background-image: linear-gradient(to bottom, #dddddd, #bbbbbb);
+    border: 1px solid #bbb;
+    border-bottom: 1px solid #999;
+    cursor: pointer;
+    text-shadow: 0 1px 0 #ddd;
+}
+.sp-container button:active {
+    border: 1px solid #aaa;
+    border-bottom: 1px solid #888;
+    -webkit-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
+    -moz-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
+    -ms-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
+    -o-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
+    box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
+}
+.sp-cancel
+{
+    font-size: 11px;
+    color: #d93f3f !important;
+    margin:0;
+    padding:2px;
+    margin-right: 5px;
+    vertical-align: middle;
+    text-decoration:none;
+
+}
+.sp-cancel:hover
+{
+    color: #d93f3f !important;
+    text-decoration: underline;
+}
+
+
+.sp-palette span:hover, .sp-palette span.sp-thumb-active
+{
+    border-color: #000;
+}
+
+.sp-preview, .sp-alpha, .sp-thumb-el
+{
+    position:relative;
+    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
+}
+.sp-preview-inner, .sp-alpha-inner, .sp-thumb-inner
+{
+    display:block;
+    position:absolute;
+    top:0;left:0;bottom:0;right:0;
+}
+
+.sp-palette .sp-thumb-inner
+{
+    background-position: 50% 50%;
+    background-repeat: no-repeat;
+}
+
+.sp-palette .sp-thumb-light.sp-thumb-active .sp-thumb-inner
+{
+    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAIVJREFUeNpiYBhsgJFMffxAXABlN5JruT4Q3wfi/0DsT64h8UD8HmpIPCWG/KemIfOJCUB+Aoacx6EGBZyHBqI+WsDCwuQ9mhxeg2A210Ntfo8klk9sOMijaURm7yc1UP2RNCMbKE9ODK1HM6iegYLkfx8pligC9lCD7KmRof0ZhjQACDAAceovrtpVBRkAAAAASUVORK5CYII=);
+}
+
+.sp-palette .sp-thumb-dark.sp-thumb-active .sp-thumb-inner
+{
+    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAAMdJREFUOE+tkgsNwzAMRMugEAahEAahEAZhEAqlEAZhEAohEAYh81X2dIm8fKpEspLGvudPOsUYpxE2BIJCroJmEW9qJ+MKaBFhEMNabSy9oIcIPwrB+afvAUFoK4H0tMaQ3XtlrggDhOVVMuT4E5MMG0FBbCEYzjYT7OxLEvIHQLY2zWwQ3D+9luyOQTfKDiFD3iUIfPk8VqrKjgAiSfGFPecrg6HN6m/iBcwiDAo7WiBeawa+Kwh7tZoSCGLMqwlSAzVDhoK+6vH4G0P5wdkAAAAASUVORK5CYII=);
+}
Index: /extensions/EasyCaptcha/template/bgrins-spectrum/spectrum.js
===================================================================
--- /extensions/EasyCaptcha/template/bgrins-spectrum/spectrum.js	(revision 24215)
+++ /extensions/EasyCaptcha/template/bgrins-spectrum/spectrum.js	(revision 24215)
@@ -0,0 +1,1935 @@
+// Spectrum Colorpicker v1.1.1
+// https://github.com/bgrins/spectrum
+// Author: Brian Grinstead
+// License: MIT
+
+(function (window, $, undefined) {
+    var defaultOpts = {
+
+        // Callbacks
+        beforeShow: noop,
+        move: noop,
+        change: noop,
+        show: noop,
+        hide: noop,
+
+        // Options
+        color: false,
+        flat: false,
+        showInput: false,
+        showButtons: true,
+        clickoutFiresChange: false,
+        showInitial: false,
+        showPalette: false,
+        showPaletteOnly: false,
+        showSelectionPalette: true,
+        localStorageKey: false,
+        appendTo: "body",
+        maxSelectionSize: 7,
+        cancelText: "cancel",
+        chooseText: "choose",
+        preferredFormat: false,
+        className: "",
+        showAlpha: false,
+        theme: "sp-light",
+        palette: ['fff', '000'],
+        selectionPalette: [],
+        disabled: false
+    },
+    spectrums = [],
+    IE = !!/msie/i.exec( window.navigator.userAgent ),
+    rgbaSupport = (function() {
+        function contains( str, substr ) {
+            return !!~('' + str).indexOf(substr);
+        }
+
+        var elem = document.createElement('div');
+        var style = elem.style;
+        style.cssText = 'background-color:rgba(0,0,0,.5)';
+        return contains(style.backgroundColor, 'rgba') || contains(style.backgroundColor, 'hsla');
+    })(),
+    replaceInput = [
+        "<div class='sp-replacer'>",
+            "<div class='sp-preview'><div class='sp-preview-inner'></div></div>",
+            "<div class='sp-dd'>&#9660;</div>",
+        "</div>"
+    ].join(''),
+    markup = (function () {
+
+        // IE does not support gradients with multiple stops, so we need to simulate
+        //  that for the rainbow slider with 8 divs that each have a single gradient
+        var gradientFix = "";
+        if (IE) {
+            for (var i = 1; i <= 6; i++) {
+                gradientFix += "<div class='sp-" + i + "'></div>";
+            }
+        }
+
+        return [
+            "<div class='sp-container sp-hidden'>",
+                "<div class='sp-palette-container'>",
+                    "<div class='sp-palette sp-thumb sp-cf'></div>",
+                "</div>",
+                "<div class='sp-picker-container'>",
+                    "<div class='sp-top sp-cf'>",
+                        "<div class='sp-fill'></div>",
+                        "<div class='sp-top-inner'>",
+                            "<div class='sp-color'>",
+                                "<div class='sp-sat'>",
+                                    "<div class='sp-val'>",
+                                        "<div class='sp-dragger'></div>",
+                                    "</div>",
+                                "</div>",
+                            "</div>",
+                            "<div class='sp-hue'>",
+                                "<div class='sp-slider'></div>",
+                                gradientFix,
+                            "</div>",
+                        "</div>",
+                        "<div class='sp-alpha'><div class='sp-alpha-inner'><div class='sp-alpha-handle'></div></div></div>",
+                    "</div>",
+                    "<div class='sp-input-container sp-cf'>",
+                        "<input class='sp-input' type='text' spellcheck='false'  />",
+                    "</div>",
+                    "<div class='sp-initial sp-thumb sp-cf'></div>",
+                    "<div class='sp-button-container sp-cf'>",
+                        "<a class='sp-cancel' href='#'></a>",
+                        "<button class='sp-choose'></button>",
+                    "</div>",
+                "</div>",
+            "</div>"
+        ].join("");
+    })();
+
+    function paletteTemplate (p, color, className) {
+        var html = [];
+        for (var i = 0; i < p.length; i++) {
+            var tiny = tinycolor(p[i]);
+            var c = tiny.toHsl().l < 0.5 ? "sp-thumb-el sp-thumb-dark" : "sp-thumb-el sp-thumb-light";
+            c += (tinycolor.equals(color, p[i])) ? " sp-thumb-active" : "";
+
+            var swatchStyle = rgbaSupport ? ("background-color:" + tiny.toRgbString()) : "filter:" + tiny.toFilter();
+            html.push('<span title="' + tiny.toRgbString() + '" data-color="' + tiny.toRgbString() + '" class="' + c + '"><span class="sp-thumb-inner" style="' + swatchStyle + ';" /></span>');
+        }
+        return "<div class='sp-cf " + className + "'>" + html.join('') + "</div>";
+    }
+
+    function hideAll() {
+        for (var i = 0; i < spectrums.length; i++) {
+            if (spectrums[i]) {
+                spectrums[i].hide();
+            }
+        }
+    }
+
+    function instanceOptions(o, callbackContext) {
+        var opts = $.extend({}, defaultOpts, o);
+        opts.callbacks = {
+            'move': bind(opts.move, callbackContext),
+            'change': bind(opts.change, callbackContext),
+            'show': bind(opts.show, callbackContext),
+            'hide': bind(opts.hide, callbackContext),
+            'beforeShow': bind(opts.beforeShow, callbackContext)
+        };
+
+        return opts;
+    }
+
+    function spectrum(element, o) {
+
+        var opts = instanceOptions(o, element),
+            flat = opts.flat,
+            showSelectionPalette = opts.showSelectionPalette,
+            localStorageKey = opts.localStorageKey,
+            theme = opts.theme,
+            callbacks = opts.callbacks,
+            resize = throttle(reflow, 10),
+            visible = false,
+            dragWidth = 0,
+            dragHeight = 0,
+            dragHelperHeight = 0,
+            slideHeight = 0,
+            slideWidth = 0,
+            alphaWidth = 0,
+            alphaSlideHelperWidth = 0,
+            slideHelperHeight = 0,
+            currentHue = 0,
+            currentSaturation = 0,
+            currentValue = 0,
+            currentAlpha = 1,
+            palette = opts.palette.slice(0),
+            paletteArray = $.isArray(palette[0]) ? palette : [palette],
+            selectionPalette = opts.selectionPalette.slice(0),
+            maxSelectionSize = opts.maxSelectionSize,
+            draggingClass = "sp-dragging",
+            shiftMovementDirection = null;
+
+        var doc = element.ownerDocument,
+            body = doc.body,
+            boundElement = $(element),
+            disabled = false,
+            container = $(markup, doc).addClass(theme),
+            dragger = container.find(".sp-color"),
+            dragHelper = container.find(".sp-dragger"),
+            slider = container.find(".sp-hue"),
+            slideHelper = container.find(".sp-slider"),
+            alphaSliderInner = container.find(".sp-alpha-inner"),
+            alphaSlider = container.find(".sp-alpha"),
+            alphaSlideHelper = container.find(".sp-alpha-handle"),
+            textInput = container.find(".sp-input"),
+            paletteContainer = container.find(".sp-palette"),
+            initialColorContainer = container.find(".sp-initial"),
+            cancelButton = container.find(".sp-cancel"),
+            chooseButton = container.find(".sp-choose"),
+            isInput = boundElement.is("input"),
+            shouldReplace = isInput && !flat,
+            replacer = (shouldReplace) ? $(replaceInput).addClass(theme).addClass(opts.className) : $([]),
+            offsetElement = (shouldReplace) ? replacer : boundElement,
+            previewElement = replacer.find(".sp-preview-inner"),
+            initialColor = opts.color || (isInput && boundElement.val()),
+            colorOnShow = false,
+            preferredFormat = opts.preferredFormat,
+            currentPreferredFormat = preferredFormat,
+            clickoutFiresChange = !opts.showButtons || opts.clickoutFiresChange;
+
+
+        function applyOptions() {
+
+            if (opts.showPaletteOnly) {
+                opts.showPalette = true;
+            }
+
+            container.toggleClass("sp-flat", flat);
+            container.toggleClass("sp-input-disabled", !opts.showInput);
+            container.toggleClass("sp-alpha-enabled", opts.showAlpha);
+            container.toggleClass("sp-buttons-disabled", !opts.showButtons);
+            container.toggleClass("sp-palette-disabled", !opts.showPalette);
+            container.toggleClass("sp-palette-only", opts.showPaletteOnly);
+            container.toggleClass("sp-initial-disabled", !opts.showInitial);
+            container.addClass(opts.className);
+
+            reflow();
+        }
+
+        function initialize() {
+
+            if (IE) {
+                container.find("*:not(input)").attr("unselectable", "on");
+            }
+
+            applyOptions();
+
+            if (shouldReplace) {
+                boundElement.after(replacer).hide();
+            }
+
+            if (flat) {
+                boundElement.after(container).hide();
+            }
+            else {
+
+                var appendTo = opts.appendTo === "parent" ? boundElement.parent() : $(opts.appendTo);
+                if (appendTo.length !== 1) {
+                    appendTo = $("body");
+                }
+
+                appendTo.append(container);
+            }
+
+            if (localStorageKey && window.localStorage) {
+
+                // Migrate old palettes over to new format.  May want to remove this eventually.
+                try {
+                    var oldPalette = window.localStorage[localStorageKey].split(",#");
+                    if (oldPalette.length > 1) {
+                        delete window.localStorage[localStorageKey];
+                        $.each(oldPalette, function(i, c) {
+                             addColorToSelectionPalette(c);
+                        });
+                    }
+                }
+                catch(e) { }
+
+                try {
+                    selectionPalette = window.localStorage[localStorageKey].split(";");
+                }
+                catch (e) { }
+            }
+
+            offsetElement.bind("click.spectrum touchstart.spectrum", function (e) {
+                if (!disabled) {
+                    toggle();
+                }
+
+                e.stopPropagation();
+
+                if (!$(e.target).is("input")) {
+                    e.preventDefault();
+                }
+            });
+
+            if(boundElement.is(":disabled") || (opts.disabled === true)) {
+                disable();
+            }
+
+            // Prevent clicks from bubbling up to document.  This would cause it to be hidden.
+            container.click(stopPropagation);
+
+            // Handle user typed input
+            textInput.change(setFromTextInput);
+            textInput.bind("paste", function () {
+                setTimeout(setFromTextInput, 1);
+            });
+            textInput.keydown(function (e) { if (e.keyCode == 13) { setFromTextInput(); } });
+
+            cancelButton.text(opts.cancelText);
+            cancelButton.bind("click.spectrum", function (e) {
+                e.stopPropagation();
+                e.preventDefault();
+                hide("cancel");
+            });
+
+            chooseButton.text(opts.chooseText);
+            chooseButton.bind("click.spectrum", function (e) {
+                e.stopPropagation();
+                e.preventDefault();
+
+                if (isValid()) {
+                    updateOriginalInput(true);
+                    hide();
+                }
+            });
+
+            draggable(alphaSlider, function (dragX, dragY, e) {
+                currentAlpha = (dragX / alphaWidth);
+                if (e.shiftKey) {
+                    currentAlpha = Math.round(currentAlpha * 10) / 10;
+                }
+
+                move();
+            });
+
+            draggable(slider, function (dragX, dragY) {
+                currentHue = parseFloat(dragY / slideHeight);
+                move();
+            }, dragStart, dragStop);
+
+            draggable(dragger, function (dragX, dragY, e) {
+
+                // shift+drag should snap the movement to either the x or y axis.
+                if (!e.shiftKey) {
+                    shiftMovementDirection = null;
+                }
+                else if (!shiftMovementDirection) {
+                    var oldDragX = currentSaturation * dragWidth;
+                    var oldDragY = dragHeight - (currentValue * dragHeight);
+                    var furtherFromX = Math.abs(dragX - oldDragX) > Math.abs(dragY - oldDragY);
+
+                    shiftMovementDirection = furtherFromX ? "x" : "y";
+                }
+
+                var setSaturation = !shiftMovementDirection || shiftMovementDirection === "x";
+                var setValue = !shiftMovementDirection || shiftMovementDirection === "y";
+
+                if (setSaturation) {
+                    currentSaturation = parseFloat(dragX / dragWidth);
+                }
+                if (setValue) {
+                    currentValue = parseFloat((dragHeight - dragY) / dragHeight);
+                }
+
+                move();
+
+            }, dragStart, dragStop);
+
+            if (!!initialColor) {
+                set(initialColor);
+
+                // In case color was black - update the preview UI and set the format
+                // since the set function will not run (default color is black).
+                updateUI();
+                currentPreferredFormat = preferredFormat || tinycolor(initialColor).format;
+
+                addColorToSelectionPalette(initialColor);
+            }
+            else {
+                updateUI();
+            }
+
+            if (flat) {
+                show();
+            }
+
+            function palletElementClick(e) {
+                if (e.data && e.data.ignore) {
+                    set($(this).data("color"));
+                    move();
+                }
+                else {
+                    set($(this).data("color"));
+                    updateOriginalInput(true);
+                    move();
+                    hide();
+                }
+
+                return false;
+            }
+
+            var paletteEvent = IE ? "mousedown.spectrum" : "click.spectrum touchstart.spectrum";
+            paletteContainer.delegate(".sp-thumb-el", paletteEvent, palletElementClick);
+            initialColorContainer.delegate(".sp-thumb-el:nth-child(1)", paletteEvent, { ignore: true }, palletElementClick);
+        }
+
+        function addColorToSelectionPalette(color) {
+            if (showSelectionPalette) {
+                var colorRgb = tinycolor(color).toRgbString();
+                if ($.inArray(colorRgb, selectionPalette) === -1) {
+                    selectionPalette.push(colorRgb);
+                    while(selectionPalette.length > maxSelectionSize) {
+                        selectionPalette.shift();
+                    }
+                }
+
+                if (localStorageKey && window.localStorage) {
+                    try {
+                        window.localStorage[localStorageKey] = selectionPalette.join(";");
+                    }
+                    catch(e) { }
+                }
+            }
+        }
+
+        function getUniqueSelectionPalette() {
+            var unique = [];
+            var p = selectionPalette;
+            var paletteLookup = {};
+            var rgb;
+
+            if (opts.showPalette) {
+
+                for (var i = 0; i < paletteArray.length; i++) {
+                    for (var j = 0; j < paletteArray[i].length; j++) {
+                        rgb = tinycolor(paletteArray[i][j]).toRgbString();
+                        paletteLookup[rgb] = true;
+                    }
+                }
+
+                for (i = 0; i < p.length; i++) {
+                    rgb = tinycolor(p[i]).toRgbString();
+
+                    if (!paletteLookup.hasOwnProperty(rgb)) {
+                        unique.push(p[i]);
+                        paletteLookup[rgb] = true;
+                    }
+                }
+            }
+
+            return unique.reverse().slice(0, opts.maxSelectionSize);
+        }
+
+        function drawPalette() {
+
+            var currentColor = get();
+
+            var html = $.map(paletteArray, function (palette, i) {
+                return paletteTemplate(palette, currentColor, "sp-palette-row sp-palette-row-" + i);
+            });
+
+            if (selectionPalette) {
+                html.push(paletteTemplate(getUniqueSelectionPalette(), currentColor, "sp-palette-row sp-palette-row-selection"));
+            }
+
+            paletteContainer.html(html.join(""));
+        }
+
+        function drawInitial() {
+            if (opts.showInitial) {
+                var initial = colorOnShow;
+                var current = get();
+                initialColorContainer.html(paletteTemplate([initial, current], current, "sp-palette-row-initial"));
+            }
+        }
+
+        function dragStart() {
+            if (dragHeight <= 0 || dragWidth <= 0 || slideHeight <= 0) {
+                reflow();
+            }
+            container.addClass(draggingClass);
+            shiftMovementDirection = null;
+        }
+
+        function dragStop() {
+            container.removeClass(draggingClass);
+        }
+
+        function setFromTextInput() {
+            var tiny = tinycolor(textInput.val());
+            if (tiny.ok) {
+                set(tiny);
+            }
+            else {
+                textInput.addClass("sp-validation-error");
+            }
+        }
+
+        function toggle() {
+            if (visible) {
+                hide();
+            }
+            else {
+                show();
+            }
+        }
+
+        function show() {
+            var event = $.Event('beforeShow.spectrum');
+
+            if (visible) {
+                reflow();
+                return;
+            }
+
+            boundElement.trigger(event, [ get() ]);
+
+            if (callbacks.beforeShow(get()) === false || event.isDefaultPrevented()) {
+                return;
+            }
+
+            hideAll();
+            visible = true;
+
+            $(doc).bind("click.spectrum", hide);
+            $(window).bind("resize.spectrum", resize);
+            replacer.addClass("sp-active");
+            container.removeClass("sp-hidden");
+
+            if (opts.showPalette) {
+                drawPalette();
+            }
+            reflow();
+            updateUI();
+
+            colorOnShow = get();
+
+            drawInitial();
+            callbacks.show(colorOnShow);
+            boundElement.trigger('show.spectrum', [ colorOnShow ]);
+        }
+
+        function hide(e) {
+
+            // Return on right click
+            if (e && e.type == "click" && e.button == 2) { return; }
+
+            // Return if hiding is unnecessary
+            if (!visible || flat) { return; }
+            visible = false;
+
+            $(doc).unbind("click.spectrum", hide);
+            $(window).unbind("resize.spectrum", resize);
+
+            replacer.removeClass("sp-active");
+            container.addClass("sp-hidden");
+
+            var colorHasChanged = !tinycolor.equals(get(), colorOnShow);
+
+            if (colorHasChanged) {
+                if (clickoutFiresChange && e !== "cancel") {
+                    updateOriginalInput(true);
+                }
+                else {
+                    revert();
+                }
+            }
+
+            callbacks.hide(get());
+            boundElement.trigger('hide.spectrum', [ get() ]);
+        }
+
+        function revert() {
+            set(colorOnShow, true);
+        }
+
+        function set(color, ignoreFormatChange) {
+            if (tinycolor.equals(color, get())) {
+                return;
+            }
+
+            var newColor = tinycolor(color);
+            var newHsv = newColor.toHsv();
+
+            currentHue = (newHsv.h % 360) / 360;
+            currentSaturation = newHsv.s;
+            currentValue = newHsv.v;
+            currentAlpha = newHsv.a;
+
+            updateUI();
+
+            if (newColor.ok && !ignoreFormatChange) {
+                currentPreferredFormat = preferredFormat || newColor.format;
+            }
+        }
+
+        function get(opts) {
+            opts = opts || { };
+            return tinycolor.fromRatio({
+                h: currentHue,
+                s: currentSaturation,
+                v: currentValue,
+                a: Math.round(currentAlpha * 100) / 100
+            }, { format: opts.format || currentPreferredFormat });
+        }
+
+        function isValid() {
+            return !textInput.hasClass("sp-validation-error");
+        }
+
+        function move() {
+            updateUI();
+
+            callbacks.move(get());
+            boundElement.trigger('move.spectrum', [ get() ]);
+        }
+
+        function updateUI() {
+
+            textInput.removeClass("sp-validation-error");
+
+            updateHelperLocations();
+
+            // Update dragger background color (gradients take care of saturation and value).
+            var flatColor = tinycolor.fromRatio({ h: currentHue, s: 1, v: 1 });
+            dragger.css("background-color", flatColor.toHexString());
+
+            // Get a format that alpha will be included in (hex and names ignore alpha)
+            var format = currentPreferredFormat;
+            if (currentAlpha < 1) {
+                if (format === "hex" || format === "hex3" || format === "hex6" || format === "name") {
+                    format = "rgb";
+                }
+            }
+
+            var realColor = get({ format: format }),
+                realHex = realColor.toHexString(),
+                realRgb = realColor.toRgbString();
+
+            // Update the replaced elements background color (with actual selected color)
+            if (rgbaSupport || realColor.alpha === 1) {
+                previewElement.css("background-color", realRgb);
+            }
+            else {
+                previewElement.css("background-color", "transparent");
+                previewElement.css("filter", realColor.toFilter());
+            }
+
+            if (opts.showAlpha) {
+                var rgb = realColor.toRgb();
+                rgb.a = 0;
+                var realAlpha = tinycolor(rgb).toRgbString();
+                var gradient = "linear-gradient(left, " + realAlpha + ", " + realHex + ")";
+
+                if (IE) {
+                    alphaSliderInner.css("filter", tinycolor(realAlpha).toFilter({ gradientType: 1 }, realHex));
+                }
+                else {
+                    alphaSliderInner.css("background", "-webkit-" + gradient);
+                    alphaSliderInner.css("background", "-moz-" + gradient);
+                    alphaSliderInner.css("background", "-ms-" + gradient);
+                    alphaSliderInner.css("background", gradient);
+                }
+            }
+
+
+            // Update the text entry input as it changes happen
+            if (opts.showInput) {
+                textInput.val(realColor.toString(format));
+            }
+
+            if (opts.showPalette) {
+                drawPalette();
+            }
+
+            drawInitial();
+        }
+
+        function updateHelperLocations() {
+            var s = currentSaturation;
+            var v = currentValue;
+
+            // Where to show the little circle in that displays your current selected color
+            var dragX = s * dragWidth;
+            var dragY = dragHeight - (v * dragHeight);
+            dragX = Math.max(
+                -dragHelperHeight,
+                Math.min(dragWidth - dragHelperHeight, dragX - dragHelperHeight)
+            );
+            dragY = Math.max(
+                -dragHelperHeight,
+                Math.min(dragHeight - dragHelperHeight, dragY - dragHelperHeight)
+            );
+            dragHelper.css({
+                "top": dragY,
+                "left": dragX
+            });
+
+            var alphaX = currentAlpha * alphaWidth;
+            alphaSlideHelper.css({
+                "left": alphaX - (alphaSlideHelperWidth / 2)
+            });
+
+            // Where to show the bar that displays your current selected hue
+            var slideY = (currentHue) * slideHeight;
+            slideHelper.css({
+                "top": slideY - slideHelperHeight
+            });
+        }
+
+        function updateOriginalInput(fireCallback) {
+            var color = get();
+
+            if (isInput) {
+                boundElement.val(color.toString(currentPreferredFormat));
+            }
+
+            var hasChanged = !tinycolor.equals(color, colorOnShow);
+            colorOnShow = color;
+
+            // Update the selection palette with the current color
+            addColorToSelectionPalette(color);
+            if (fireCallback && hasChanged) {
+                callbacks.change(color);
+                boundElement.trigger('change', [ color ]);
+            }
+        }
+
+        function reflow() {
+            dragWidth = dragger.width();
+            dragHeight = dragger.height();
+            dragHelperHeight = dragHelper.height();
+            slideWidth = slider.width();
+            slideHeight = slider.height();
+            slideHelperHeight = slideHelper.height();
+            alphaWidth = alphaSlider.width();
+            alphaSlideHelperWidth = alphaSlideHelper.width();
+
+            if (!flat) {
+                container.css("position", "absolute");
+                container.offset(getOffset(container, offsetElement));
+            }
+
+            updateHelperLocations();
+        }
+
+        function destroy() {
+            boundElement.show();
+            offsetElement.unbind("click.spectrum touchstart.spectrum");
+            container.remove();
+            replacer.remove();
+            spectrums[spect.id] = null;
+        }
+
+        function option(optionName, optionValue) {
+            if (optionName === undefined) {
+                return $.extend({}, opts);
+            }
+            if (optionValue === undefined) {
+                return opts[optionName];
+            }
+
+            opts[optionName] = optionValue;
+            applyOptions();
+        }
+
+        function enable() {
+            disabled = false;
+            boundElement.attr("disabled", false);
+            offsetElement.removeClass("sp-disabled");
+        }
+
+        function disable() {
+            hide();
+            disabled = true;
+            boundElement.attr("disabled", true);
+            offsetElement.addClass("sp-disabled");
+        }
+
+        initialize();
+
+        var spect = {
+            show: show,
+            hide: hide,
+            toggle: toggle,
+            reflow: reflow,
+            option: option,
+            enable: enable,
+            disable: disable,
+            set: function (c) {
+                set(c);
+                updateOriginalInput();
+            },
+            get: get,
+            destroy: destroy,
+            container: container
+        };
+
+        spect.id = spectrums.push(spect) - 1;
+
+        return spect;
+    }
+
+    /**
+    * checkOffset - get the offset below/above and left/right element depending on screen position
+    * Thanks https://github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.datepicker.js
+    */
+    function getOffset(picker, input) {
+        var extraY = 0;
+        var dpWidth = picker.outerWidth();
+        var dpHeight = picker.outerHeight();
+        var inputHeight = input.outerHeight();
+        var doc = picker[0].ownerDocument;
+        var docElem = doc.documentElement;
+        var viewWidth = docElem.clientWidth + $(doc).scrollLeft();
+        var viewHeight = docElem.clientHeight + $(doc).scrollTop();
+        var offset = input.offset();
+        offset.top += inputHeight;
+
+        offset.left -=
+            Math.min(offset.left, (offset.left + dpWidth > viewWidth && viewWidth > dpWidth) ?
+            Math.abs(offset.left + dpWidth - viewWidth) : 0);
+
+        offset.top -=
+            Math.min(offset.top, ((offset.top + dpHeight > viewHeight && viewHeight > dpHeight) ?
+            Math.abs(dpHeight + inputHeight - extraY) : extraY));
+
+        return offset;
+    }
+
+    /**
+    * noop - do nothing
+    */
+    function noop() {
+
+    }
+
+    /**
+    * stopPropagation - makes the code only doing this a little easier to read in line
+    */
+    function stopPropagation(e) {
+        e.stopPropagation();
+    }
+
+    /**
+    * Create a function bound to a given object
+    * Thanks to underscore.js
+    */
+    function bind(func, obj) {
+        var slice = Array.prototype.slice;
+        var args = slice.call(arguments, 2);
+        return function () {
+            return func.apply(obj, args.concat(slice.call(arguments)));
+        };
+    }
+
+    /**
+    * Lightweight drag helper.  Handles containment within the element, so that
+    * when dragging, the x is within [0,element.width] and y is within [0,element.height]
+    */
+    function draggable(element, onmove, onstart, onstop) {
+        onmove = onmove || function () { };
+        onstart = onstart || function () { };
+        onstop = onstop || function () { };
+        var doc = element.ownerDocument || document;
+        var dragging = false;
+        var offset = {};
+        var maxHeight = 0;
+        var maxWidth = 0;
+        var hasTouch = ('ontouchstart' in window);
+
+        var duringDragEvents = {};
+        duringDragEvents["selectstart"] = prevent;
+        duringDragEvents["dragstart"] = prevent;
+        duringDragEvents["touchmove mousemove"] = move;
+        duringDragEvents["touchend mouseup"] = stop;
+
+        function prevent(e) {
+            if (e.stopPropagation) {
+                e.stopPropagation();
+            }
+            if (e.preventDefault) {
+                e.preventDefault();
+            }
+            e.returnValue = false;
+        }
+
+        function move(e) {
+            if (dragging) {
+                // Mouseup happened outside of window
+                if (IE && document.documentMode < 9 && !e.button) {
+                    return stop();
+                }
+
+                var touches = e.originalEvent.touches;
+                var pageX = touches ? touches[0].pageX : e.pageX;
+                var pageY = touches ? touches[0].pageY : e.pageY;
+
+                var dragX = Math.max(0, Math.min(pageX - offset.left, maxWidth));
+                var dragY = Math.max(0, Math.min(pageY - offset.top, maxHeight));
+
+                if (hasTouch) {
+                    // Stop scrolling in iOS
+                    prevent(e);
+                }
+
+                onmove.apply(element, [dragX, dragY, e]);
+            }
+        }
+        function start(e) {
+            var rightclick = (e.which) ? (e.which == 3) : (e.button == 2);
+            var touches = e.originalEvent.touches;
+
+            if (!rightclick && !dragging) {
+                if (onstart.apply(element, arguments) !== false) {
+                    dragging = true;
+                    maxHeight = $(element).height();
+                    maxWidth = $(element).width();
+                    offset = $(element).offset();
+
+                    $(doc).bind(duringDragEvents);
+                    $(doc.body).addClass("sp-dragging");
+
+                    if (!hasTouch) {
+                        move(e);
+                    }
+
+                    prevent(e);
+                }
+            }
+        }
+        function stop() {
+            if (dragging) {
+                $(doc).unbind(duringDragEvents);
+                $(doc.body).removeClass("sp-dragging");
+                onstop.apply(element, arguments);
+            }
+            dragging = false;
+        }
+
+        $(element).bind("touchstart mousedown", start);
+    }
+
+    function throttle(func, wait, debounce) {
+        var timeout;
+        return function () {
+            var context = this, args = arguments;
+            var throttler = function () {
+                timeout = null;
+                func.apply(context, args);
+            };
+            if (debounce) clearTimeout(timeout);
+            if (debounce || !timeout) timeout = setTimeout(throttler, wait);
+        };
+    }
+
+
+    function log(){/* jshint -W021 */if(window.console){if(Function.prototype.bind)log=Function.prototype.bind.call(console.log,console);else log=function(){Function.prototype.apply.call(console.log,console,arguments);};log.apply(this,arguments);}}
+
+    /**
+    * Define a jQuery plugin
+    */
+    var dataID = "spectrum.id";
+    $.fn.spectrum = function (opts, extra) {
+
+        if (typeof opts == "string") {
+
+            var returnValue = this;
+            var args = Array.prototype.slice.call( arguments, 1 );
+
+            this.each(function () {
+                var spect = spectrums[$(this).data(dataID)];
+                if (spect) {
+
+                    var method = spect[opts];
+                    if (!method) {
+                        throw new Error( "Spectrum: no such method: '" + opts + "'" );
+                    }
+
+                    if (opts == "get") {
+                        returnValue = spect.get();
+                    }
+                    else if (opts == "container") {
+                        returnValue = spect.container;
+                    }
+                    else if (opts == "option") {
+                        returnValue = spect.option.apply(spect, args);
+                    }
+                    else if (opts == "destroy") {
+                        spect.destroy();
+                        $(this).removeData(dataID);
+                    }
+                    else {
+                        method.apply(spect, args);
+                    }
+                }
+            });
+
+            return returnValue;
+        }
+
+        // Initializing a new instance of spectrum
+        return this.spectrum("destroy").each(function () {
+            var spect = spectrum(this, opts);
+            $(this).data(dataID, spect.id);
+        });
+    };
+
+    $.fn.spectrum.load = true;
+    $.fn.spectrum.loadOpts = {};
+    $.fn.spectrum.draggable = draggable;
+    $.fn.spectrum.defaults = defaultOpts;
+
+    $.spectrum = { };
+    $.spectrum.localization = { };
+    $.spectrum.palettes = { };
+
+    $.fn.spectrum.processNativeColorInputs = function () {
+        var colorInput = $("<input type='color' value='!' />")[0];
+        var supportsColor = colorInput.type === "color" && colorInput.value != "!";
+
+        if (!supportsColor) {
+            $("input[type=color]").spectrum({
+                preferredFormat: "hex6"
+            });
+        }
+    };
+
+    // TinyColor v0.9.16
+    // https://github.com/bgrins/TinyColor
+    // 2013-08-10, Brian Grinstead, MIT License
+
+    (function() {
+
+    var trimLeft = /^[\s,#]+/,
+        trimRight = /\s+$/,
+        tinyCounter = 0,
+        math = Math,
+        mathRound = math.round,
+        mathMin = math.min,
+        mathMax = math.max,
+        mathRandom = math.random;
+
+    function tinycolor (color, opts) {
+
+        color = (color) ? color : '';
+        opts = opts || { };
+
+        // If input is already a tinycolor, return itself
+        if (typeof color == "object" && color.hasOwnProperty("_tc_id")) {
+           return color;
+        }
+
+        var rgb = inputToRGB(color);
+        var r = rgb.r,
+            g = rgb.g,
+            b = rgb.b,
+            a = rgb.a,
+            roundA = mathRound(100*a) / 100,
+            format = opts.format || rgb.format;
+
+        // Don't let the range of [0,255] come back in [0,1].
+        // Potentially lose a little bit of precision here, but will fix issues where
+        // .5 gets interpreted as half of the total, instead of half of 1
+        // If it was supposed to be 128, this was already taken care of by `inputToRgb`
+        if (r < 1) { r = mathRound(r); }
+        if (g < 1) { g = mathRound(g); }
+        if (b < 1) { b = mathRound(b); }
+
+        return {
+            ok: rgb.ok,
+            format: format,
+            _tc_id: tinyCounter++,
+            alpha: a,
+            getAlpha: function() {
+                return a;
+            },
+            setAlpha: function(value) {
+                a = boundAlpha(value);
+                roundA = mathRound(100*a) / 100;
+            },
+            toHsv: function() {
+                var hsv = rgbToHsv(r, g, b);
+                return { h: hsv.h * 360, s: hsv.s, v: hsv.v, a: a };
+            },
+            toHsvString: function() {
+                var hsv = rgbToHsv(r, g, b);
+                var h = mathRound(hsv.h * 360), s = mathRound(hsv.s * 100), v = mathRound(hsv.v * 100);
+                return (a == 1) ?
+                  "hsv("  + h + ", " + s + "%, " + v + "%)" :
+                  "hsva(" + h + ", " + s + "%, " + v + "%, "+ roundA + ")";
+            },
+            toHsl: function() {
+                var hsl = rgbToHsl(r, g, b);
+                return { h: hsl.h * 360, s: hsl.s, l: hsl.l, a: a };
+            },
+            toHslString: function() {
+                var hsl = rgbToHsl(r, g, b);
+                var h = mathRound(hsl.h * 360), s = mathRound(hsl.s * 100), l = mathRound(hsl.l * 100);
+                return (a == 1) ?
+                  "hsl("  + h + ", " + s + "%, " + l + "%)" :
+                  "hsla(" + h + ", " + s + "%, " + l + "%, "+ roundA + ")";
+            },
+            toHex: function(allow3Char) {
+                return rgbToHex(r, g, b, allow3Char);
+            },
+            toHexString: function(allow3Char) {
+                return '#' + rgbToHex(r, g, b, allow3Char);
+            },
+            toRgb: function() {
+                return { r: mathRound(r), g: mathRound(g), b: mathRound(b), a: a };
+            },
+            toRgbString: function() {
+                return (a == 1) ?
+                  "rgb("  + mathRound(r) + ", " + mathRound(g) + ", " + mathRound(b) + ")" :
+                  "rgba(" + mathRound(r) + ", " + mathRound(g) + ", " + mathRound(b) + ", " + roundA + ")";
+            },
+            toPercentageRgb: function() {
+                return { r: mathRound(bound01(r, 255) * 100) + "%", g: mathRound(bound01(g, 255) * 100) + "%", b: mathRound(bound01(b, 255) * 100) + "%", a: a };
+            },
+            toPercentageRgbString: function() {
+                return (a == 1) ?
+                  "rgb("  + mathRound(bound01(r, 255) * 100) + "%, " + mathRound(bound01(g, 255) * 100) + "%, " + mathRound(bound01(b, 255) * 100) + "%)" :
+                  "rgba(" + mathRound(bound01(r, 255) * 100) + "%, " + mathRound(bound01(g, 255) * 100) + "%, " + mathRound(bound01(b, 255) * 100) + "%, " + roundA + ")";
+            },
+            toName: function() {
+                if (a === 0) {
+                    return "transparent";
+                }
+
+                return hexNames[rgbToHex(r, g, b, true)] || false;
+            },
+            toFilter: function(secondColor) {
+                var hex = rgbToHex(r, g, b);
+                var secondHex = hex;
+                var alphaHex = Math.round(parseFloat(a) * 255).toString(16);
+                var secondAlphaHex = alphaHex;
+                var gradientType = opts && opts.gradientType ? "GradientType = 1, " : "";
+
+                if (secondColor) {
+                    var s = tinycolor(secondColor);
+                    secondHex = s.toHex();
+                    secondAlphaHex = Math.round(parseFloat(s.alpha) * 255).toString(16);
+                }
+
+                return "progid:DXImageTransform.Microsoft.gradient("+gradientType+"startColorstr=#" + pad2(alphaHex) + hex + ",endColorstr=#" + pad2(secondAlphaHex) + secondHex + ")";
+            },
+            toString: function(format) {
+                var formatSet = !!format;
+                format = format || this.format;
+
+                var formattedString = false;
+                var hasAlphaAndFormatNotSet = !formatSet && a < 1 && a > 0;
+                var formatWithAlpha = hasAlphaAndFormatNotSet && (format === "hex" || format === "hex6" || format === "hex3" || format === "name");
+
+                if (format === "rgb") {
+                    formattedString = this.toRgbString();
+                }
+                if (format === "prgb") {
+                    formattedString = this.toPercentageRgbString();
+                }
+                if (format === "hex" || format === "hex6") {
+                    formattedString = this.toHexString();
+                }
+                if (format === "hex3") {
+                    formattedString = this.toHexString(true);
+                }
+                if (format === "name") {
+                    formattedString = this.toName();
+                }
+                if (format === "hsl") {
+                    formattedString = this.toHslString();
+                }
+                if (format === "hsv") {
+                    formattedString = this.toHsvString();
+                }
+
+                if (formatWithAlpha) {
+                    return this.toRgbString();
+                }
+
+                return formattedString || this.toHexString();
+            }
+        };
+    }
+
+    // If input is an object, force 1 into "1.0" to handle ratios properly
+    // String input requires "1.0" as input, so 1 will be treated as 1
+    tinycolor.fromRatio = function(color, opts) {
+        if (typeof color == "object") {
+            var newColor = {};
+            for (var i in color) {
+                if (color.hasOwnProperty(i)) {
+                    if (i === "a") {
+                        newColor[i] = color[i];
+                    }
+                    else {
+                        newColor[i] = convertToPercentage(color[i]);
+                    }
+                }
+            }
+            color = newColor;
+        }
+
+        return tinycolor(color, opts);
+    };
+
+    // Given a string or object, convert that input to RGB
+    // Possible string inputs:
+    //
+    //     "red"
+    //     "#f00" or "f00"
+    //     "#ff0000" or "ff0000"
+    //     "rgb 255 0 0" or "rgb (255, 0, 0)"
+    //     "rgb 1.0 0 0" or "rgb (1, 0, 0)"
+    //     "rgba (255, 0, 0, 1)" or "rgba 255, 0, 0, 1"
+    //     "rgba (1.0, 0, 0, 1)" or "rgba 1.0, 0, 0, 1"
+    //     "hsl(0, 100%, 50%)" or "hsl 0 100% 50%"
+    //     "hsla(0, 100%, 50%, 1)" or "hsla 0 100% 50%, 1"
+    //     "hsv(0, 100%, 100%)" or "hsv 0 100% 100%"
+    //
+    function inputToRGB(color) {
+
+        var rgb = { r: 0, g: 0, b: 0 };
+        var a = 1;
+        var ok = false;
+        var format = false;
+
+        if (typeof color == "string") {
+            color = stringInputToObject(color);
+        }
+
+        if (typeof color == "object") {
+            if (color.hasOwnProperty("r") && color.hasOwnProperty("g") && color.hasOwnProperty("b")) {
+                rgb = rgbToRgb(color.r, color.g, color.b);
+                ok = true;
+                format = String(color.r).substr(-1) === "%" ? "prgb" : "rgb";
+            }
+            else if (color.hasOwnProperty("h") && color.hasOwnProperty("s") && color.hasOwnProperty("v")) {
+                color.s = convertToPercentage(color.s);
+                color.v = convertToPercentage(color.v);
+                rgb = hsvToRgb(color.h, color.s, color.v);
+                ok = true;
+                format = "hsv";
+            }
+            else if (color.hasOwnProperty("h") && color.hasOwnProperty("s") && color.hasOwnProperty("l")) {
+                color.s = convertToPercentage(color.s);
+                color.l = convertToPercentage(color.l);
+                rgb = hslToRgb(color.h, color.s, color.l);
+                ok = true;
+                format = "hsl";
+            }
+
+            if (color.hasOwnProperty("a")) {
+                a = color.a;
+            }
+        }
+
+        a = boundAlpha(a);
+
+        return {
+            ok: ok,
+            format: color.format || format,
+            r: mathMin(255, mathMax(rgb.r, 0)),
+            g: mathMin(255, mathMax(rgb.g, 0)),
+            b: mathMin(255, mathMax(rgb.b, 0)),
+            a: a
+        };
+    }
+
+
+    // Conversion Functions
+    // --------------------
+
+    // `rgbToHsl`, `rgbToHsv`, `hslToRgb`, `hsvToRgb` modified from:
+    // <http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript>
+
+    // `rgbToRgb`
+    // Handle bounds / percentage checking to conform to CSS color spec
+    // <http://www.w3.org/TR/css3-color/>
+    // *Assumes:* r, g, b in [0, 255] or [0, 1]
+    // *Returns:* { r, g, b } in [0, 255]
+    function rgbToRgb(r, g, b){
+        return {
+            r: bound01(r, 255) * 255,
+            g: bound01(g, 255) * 255,
+            b: bound01(b, 255) * 255
+        };
+    }
+
+    // `rgbToHsl`
+    // Converts an RGB color value to HSL.
+    // *Assumes:* r, g, and b are contained in [0, 255] or [0, 1]
+    // *Returns:* { h, s, l } in [0,1]
+    function rgbToHsl(r, g, b) {
+
+        r = bound01(r, 255);
+        g = bound01(g, 255);
+        b = bound01(b, 255);
+
+        var max = mathMax(r, g, b), min = mathMin(r, g, b);
+        var h, s, l = (max + min) / 2;
+
+        if(max == min) {
+            h = s = 0; // achromatic
+        }
+        else {
+            var d = max - min;
+            s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+            switch(max) {
+                case r: h = (g - b) / d + (g < b ? 6 : 0); break;
+                case g: h = (b - r) / d + 2; break;
+                case b: h = (r - g) / d + 4; break;
+            }
+
+            h /= 6;
+        }
+
+        return { h: h, s: s, l: l };
+    }
+
+    // `hslToRgb`
+    // Converts an HSL color value to RGB.
+    // *Assumes:* h is contained in [0, 1] or [0, 360] and s and l are contained [0, 1] or [0, 100]
+    // *Returns:* { r, g, b } in the set [0, 255]
+    function hslToRgb(h, s, l) {
+        var r, g, b;
+
+        h = bound01(h, 360);
+        s = bound01(s, 100);
+        l = bound01(l, 100);
+
+        function hue2rgb(p, q, t) {
+            if(t < 0) t += 1;
+            if(t > 1) t -= 1;
+            if(t < 1/6) return p + (q - p) * 6 * t;
+            if(t < 1/2) return q;
+            if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
+            return p;
+        }
+
+        if(s === 0) {
+            r = g = b = l; // achromatic
+        }
+        else {
+            var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+            var p = 2 * l - q;
+            r = hue2rgb(p, q, h + 1/3);
+            g = hue2rgb(p, q, h);
+            b = hue2rgb(p, q, h - 1/3);
+        }
+
+        return { r: r * 255, g: g * 255, b: b * 255 };
+    }
+
+    // `rgbToHsv`
+    // Converts an RGB color value to HSV
+    // *Assumes:* r, g, and b are contained in the set [0, 255] or [0, 1]
+    // *Returns:* { h, s, v } in [0,1]
+    function rgbToHsv(r, g, b) {
+
+        r = bound01(r, 255);
+        g = bound01(g, 255);
+        b = bound01(b, 255);
+
+        var max = mathMax(r, g, b), min = mathMin(r, g, b);
+        var h, s, v = max;
+
+        var d = max - min;
+        s = max === 0 ? 0 : d / max;
+
+        if(max == min) {
+            h = 0; // achromatic
+        }
+        else {
+            switch(max) {
+                case r: h = (g - b) / d + (g < b ? 6 : 0); break;
+                case g: h = (b - r) / d + 2; break;
+                case b: h = (r - g) / d + 4; break;
+            }
+            h /= 6;
+        }
+        return { h: h, s: s, v: v };
+    }
+
+    // `hsvToRgb`
+    // Converts an HSV color value to RGB.
+    // *Assumes:* h is contained in [0, 1] or [0, 360] and s and v are contained in [0, 1] or [0, 100]
+    // *Returns:* { r, g, b } in the set [0, 255]
+     function hsvToRgb(h, s, v) {
+
+        h = bound01(h, 360) * 6;
+        s = bound01(s, 100);
+        v = bound01(v, 100);
+
+        var i = math.floor(h),
+            f = h - i,
+            p = v * (1 - s),
+            q = v * (1 - f * s),
+            t = v * (1 - (1 - f) * s),
+            mod = i % 6,
+            r = [v, q, p, p, t, v][mod],
+            g = [t, v, v, q, p, p][mod],
+            b = [p, p, t, v, v, q][mod];
+
+        return { r: r * 255, g: g * 255, b: b * 255 };
+    }
+
+    // `rgbToHex`
+    // Converts an RGB color to hex
+    // Assumes r, g, and b are contained in the set [0, 255]
+    // Returns a 3 or 6 character hex
+    function rgbToHex(r, g, b, allow3Char) {
+
+        var hex = [
+            pad2(mathRound(r).toString(16)),
+            pad2(mathRound(g).toString(16)),
+            pad2(mathRound(b).toString(16))
+        ];
+
+        // Return a 3 character hex if possible
+        if (allow3Char && hex[0].charAt(0) == hex[0].charAt(1) && hex[1].charAt(0) == hex[1].charAt(1) && hex[2].charAt(0) == hex[2].charAt(1)) {
+            return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0);
+        }
+
+        return hex.join("");
+    }
+
+    // `equals`
+    // Can be called with any tinycolor input
+    tinycolor.equals = function (color1, color2) {
+        if (!color1 || !color2) { return false; }
+        return tinycolor(color1).toRgbString() == tinycolor(color2).toRgbString();
+    };
+    tinycolor.random = function() {
+        return tinycolor.fromRatio({
+            r: mathRandom(),
+            g: mathRandom(),
+            b: mathRandom()
+        });
+    };
+
+
+    // Modification Functions
+    // ----------------------
+    // Thanks to less.js for some of the basics here
+    // <https://github.com/cloudhead/less.js/blob/master/lib/less/functions.js>
+
+    tinycolor.desaturate = function (color, amount) {
+        amount = (amount === 0) ? 0 : (amount || 10);
+        var hsl = tinycolor(color).toHsl();
+        hsl.s -= amount / 100;
+        hsl.s = clamp01(hsl.s);
+        return tinycolor(hsl);
+    };
+    tinycolor.saturate = function (color, amount) {
+        amount = (amount === 0) ? 0 : (amount || 10);
+        var hsl = tinycolor(color).toHsl();
+        hsl.s += amount / 100;
+        hsl.s = clamp01(hsl.s);
+        return tinycolor(hsl);
+    };
+    tinycolor.greyscale = function(color) {
+        return tinycolor.desaturate(color, 100);
+    };
+    tinycolor.lighten = function(color, amount) {
+        amount = (amount === 0) ? 0 : (amount || 10);
+        var hsl = tinycolor(color).toHsl();
+        hsl.l += amount / 100;
+        hsl.l = clamp01(hsl.l);
+        return tinycolor(hsl);
+    };
+    tinycolor.darken = function (color, amount) {
+        amount = (amount === 0) ? 0 : (amount || 10);
+        var hsl = tinycolor(color).toHsl();
+        hsl.l -= amount / 100;
+        hsl.l = clamp01(hsl.l);
+        return tinycolor(hsl);
+    };
+    tinycolor.complement = function(color) {
+        var hsl = tinycolor(color).toHsl();
+        hsl.h = (hsl.h + 180) % 360;
+        return tinycolor(hsl);
+    };
+
+
+    // Combination Functions
+    // ---------------------
+    // Thanks to jQuery xColor for some of the ideas behind these
+    // <https://github.com/infusion/jQuery-xcolor/blob/master/jquery.xcolor.js>
+
+    tinycolor.triad = function(color) {
+        var hsl = tinycolor(color).toHsl();
+        var h = hsl.h;
+        return [
+            tinycolor(color),
+            tinycolor({ h: (h + 120) % 360, s: hsl.s, l: hsl.l }),
+            tinycolor({ h: (h + 240) % 360, s: hsl.s, l: hsl.l })
+        ];
+    };
+    tinycolor.tetrad = function(color) {
+        var hsl = tinycolor(color).toHsl();
+        var h = hsl.h;
+        return [
+            tinycolor(color),
+            tinycolor({ h: (h + 90) % 360, s: hsl.s, l: hsl.l }),
+            tinycolor({ h: (h + 180) % 360, s: hsl.s, l: hsl.l }),
+            tinycolor({ h: (h + 270) % 360, s: hsl.s, l: hsl.l })
+        ];
+    };
+    tinycolor.splitcomplement = function(color) {
+        var hsl = tinycolor(color).toHsl();
+        var h = hsl.h;
+        return [
+            tinycolor(color),
+            tinycolor({ h: (h + 72) % 360, s: hsl.s, l: hsl.l}),
+            tinycolor({ h: (h + 216) % 360, s: hsl.s, l: hsl.l})
+        ];
+    };
+    tinycolor.analogous = function(color, results, slices) {
+        results = results || 6;
+        slices = slices || 30;
+
+        var hsl = tinycolor(color).toHsl();
+        var part = 360 / slices;
+        var ret = [tinycolor(color)];
+
+        for (hsl.h = ((hsl.h - (part * results >> 1)) + 720) % 360; --results; ) {
+            hsl.h = (hsl.h + part) % 360;
+            ret.push(tinycolor(hsl));
+        }
+        return ret;
+    };
+    tinycolor.monochromatic = function(color, results) {
+        results = results || 6;
+        var hsv = tinycolor(color).toHsv();
+        var h = hsv.h, s = hsv.s, v = hsv.v;
+        var ret = [];
+        var modification = 1 / results;
+
+        while (results--) {
+            ret.push(tinycolor({ h: h, s: s, v: v}));
+            v = (v + modification) % 1;
+        }
+
+        return ret;
+    };
+
+
+    // Readability Functions
+    // ---------------------
+    // <http://www.w3.org/TR/AERT#color-contrast>
+
+    // `readability`
+    // Analyze the 2 colors and returns an object with the following properties:
+    //    `brightness`: difference in brightness between the two colors
+    //    `color`: difference in color/hue between the two colors
+    tinycolor.readability = function(color1, color2) {
+        var a = tinycolor(color1).toRgb();
+        var b = tinycolor(color2).toRgb();
+        var brightnessA = (a.r * 299 + a.g * 587 + a.b * 114) / 1000;
+        var brightnessB = (b.r * 299 + b.g * 587 + b.b * 114) / 1000;
+        var colorDiff = (
+            Math.max(a.r, b.r) - Math.min(a.r, b.r) +
+            Math.max(a.g, b.g) - Math.min(a.g, b.g) +
+            Math.max(a.b, b.b) - Math.min(a.b, b.b)
+        );
+
+        return {
+            brightness: Math.abs(brightnessA - brightnessB),
+            color: colorDiff
+        };
+    };
+
+    // `readable`
+    // http://www.w3.org/TR/AERT#color-contrast
+    // Ensure that foreground and background color combinations provide sufficient contrast.
+    // *Example*
+    //    tinycolor.readable("#000", "#111") => false
+    tinycolor.readable = function(color1, color2) {
+        var readability = tinycolor.readability(color1, color2);
+        return readability.brightness > 125 && readability.color > 500;
+    };
+
+    // `mostReadable`
+    // Given a base color and a list of possible foreground or background
+    // colors for that base, returns the most readable color.
+    // *Example*
+    //    tinycolor.mostReadable("#123", ["#fff", "#000"]) => "#000"
+    tinycolor.mostReadable = function(baseColor, colorList) {
+        var bestColor = null;
+        var bestScore = 0;
+        var bestIsReadable = false;
+        for (var i=0; i < colorList.length; i++) {
+
+            // We normalize both around the "acceptable" breaking point,
+            // but rank brightness constrast higher than hue.
+
+            var readability = tinycolor.readability(baseColor, colorList[i]);
+            var readable = readability.brightness > 125 && readability.color > 500;
+            var score = 3 * (readability.brightness / 125) + (readability.color / 500);
+
+            if ((readable && ! bestIsReadable) ||
+                (readable && bestIsReadable && score > bestScore) ||
+                ((! readable) && (! bestIsReadable) && score > bestScore)) {
+                bestIsReadable = readable;
+                bestScore = score;
+                bestColor = tinycolor(colorList[i]);
+            }
+        }
+        return bestColor;
+    };
+
+
+    // Big List of Colors
+    // ------------------
+    // <http://www.w3.org/TR/css3-color/#svg-color>
+    var names = tinycolor.names = {
+        aliceblue: "f0f8ff",
+        antiquewhite: "faebd7",
+        aqua: "0ff",
+        aquamarine: "7fffd4",
+        azure: "f0ffff",
+        beige: "f5f5dc",
+        bisque: "ffe4c4",
+        black: "000",
+        blanchedalmond: "ffebcd",
+        blue: "00f",
+        blueviolet: "8a2be2",
+        brown: "a52a2a",
+        burlywood: "deb887",
+        burntsienna: "ea7e5d",
+        cadetblue: "5f9ea0",
+        chartreuse: "7fff00",
+        chocolate: "d2691e",
+        coral: "ff7f50",
+        cornflowerblue: "6495ed",
+        cornsilk: "fff8dc",
+        crimson: "dc143c",
+        cyan: "0ff",
+        darkblue: "00008b",
+        darkcyan: "008b8b",
+        darkgoldenrod: "b8860b",
+        darkgray: "a9a9a9",
+        darkgreen: "006400",
+        darkgrey: "a9a9a9",
+        darkkhaki: "bdb76b",
+        darkmagenta: "8b008b",
+        darkolivegreen: "556b2f",
+        darkorange: "ff8c00",
+        darkorchid: "9932cc",
+        darkred: "8b0000",
+        darksalmon: "e9967a",
+        darkseagreen: "8fbc8f",
+        darkslateblue: "483d8b",
+        darkslategray: "2f4f4f",
+        darkslategrey: "2f4f4f",
+        darkturquoise: "00ced1",
+        darkviolet: "9400d3",
+        deeppink: "ff1493",
+        deepskyblue: "00bfff",
+        dimgray: "696969",
+        dimgrey: "696969",
+        dodgerblue: "1e90ff",
+        firebrick: "b22222",
+        floralwhite: "fffaf0",
+        forestgreen: "228b22",
+        fuchsia: "f0f",
+        gainsboro: "dcdcdc",
+        ghostwhite: "f8f8ff",
+        gold: "ffd700",
+        goldenrod: "daa520",
+        gray: "808080",
+        green: "008000",
+        greenyellow: "adff2f",
+        grey: "808080",
+        honeydew: "f0fff0",
+        hotpink: "ff69b4",
+        indianred: "cd5c5c",
+        indigo: "4b0082",
+        ivory: "fffff0",
+        khaki: "f0e68c",
+        lavender: "e6e6fa",
+        lavenderblush: "fff0f5",
+        lawngreen: "7cfc00",
+        lemonchiffon: "fffacd",
+        lightblue: "add8e6",
+        lightcoral: "f08080",
+        lightcyan: "e0ffff",
+        lightgoldenrodyellow: "fafad2",
+        lightgray: "d3d3d3",
+        lightgreen: "90ee90",
+        lightgrey: "d3d3d3",
+        lightpink: "ffb6c1",
+        lightsalmon: "ffa07a",
+        lightseagreen: "20b2aa",
+        lightskyblue: "87cefa",
+        lightslategray: "789",
+        lightslategrey: "789",
+        lightsteelblue: "b0c4de",
+        lightyellow: "ffffe0",
+        lime: "0f0",
+        limegreen: "32cd32",
+        linen: "faf0e6",
+        magenta: "f0f",
+        maroon: "800000",
+        mediumaquamarine: "66cdaa",
+        mediumblue: "0000cd",
+        mediumorchid: "ba55d3",
+        mediumpurple: "9370db",
+        mediumseagreen: "3cb371",
+        mediumslateblue: "7b68ee",
+        mediumspringgreen: "00fa9a",
+        mediumturquoise: "48d1cc",
+        mediumvioletred: "c71585",
+        midnightblue: "191970",
+        mintcream: "f5fffa",
+        mistyrose: "ffe4e1",
+        moccasin: "ffe4b5",
+        navajowhite: "ffdead",
+        navy: "000080",
+        oldlace: "fdf5e6",
+        olive: "808000",
+        olivedrab: "6b8e23",
+        orange: "ffa500",
+        orangered: "ff4500",
+        orchid: "da70d6",
+        palegoldenrod: "eee8aa",
+        palegreen: "98fb98",
+        paleturquoise: "afeeee",
+        palevioletred: "db7093",
+        papayawhip: "ffefd5",
+        peachpuff: "ffdab9",
+        peru: "cd853f",
+        pink: "ffc0cb",
+        plum: "dda0dd",
+        powderblue: "b0e0e6",
+        purple: "800080",
+        red: "f00",
+        rosybrown: "bc8f8f",
+        royalblue: "4169e1",
+        saddlebrown: "8b4513",
+        salmon: "fa8072",
+        sandybrown: "f4a460",
+        seagreen: "2e8b57",
+        seashell: "fff5ee",
+        sienna: "a0522d",
+        silver: "c0c0c0",
+        skyblue: "87ceeb",
+        slateblue: "6a5acd",
+        slategray: "708090",
+        slategrey: "708090",
+        snow: "fffafa",
+        springgreen: "00ff7f",
+        steelblue: "4682b4",
+        tan: "d2b48c",
+        teal: "008080",
+        thistle: "d8bfd8",
+        tomato: "ff6347",
+        turquoise: "40e0d0",
+        violet: "ee82ee",
+        wheat: "f5deb3",
+        white: "fff",
+        whitesmoke: "f5f5f5",
+        yellow: "ff0",
+        yellowgreen: "9acd32"
+    };
+
+    // Make it easy to access colors via `hexNames[hex]`
+    var hexNames = tinycolor.hexNames = flip(names);
+
+
+    // Utilities
+    // ---------
+
+    // `{ 'name1': 'val1' }` becomes `{ 'val1': 'name1' }`
+    function flip(o) {
+        var flipped = { };
+        for (var i in o) {
+            if (o.hasOwnProperty(i)) {
+                flipped[o[i]] = i;
+            }
+        }
+        return flipped;
+    }
+
+    // Return a valid alpha value [0,1] with all invalid values being set to 1
+    function boundAlpha(a) {
+        a = parseFloat(a);
+
+        if (isNaN(a) || a < 0 || a > 1) {
+            a = 1;
+        }
+
+        return a;
+    }
+
+    // Take input from [0, n] and return it as [0, 1]
+    function bound01(n, max) {
+        if (isOnePointZero(n)) { n = "100%"; }
+
+        var processPercent = isPercentage(n);
+        n = mathMin(max, mathMax(0, parseFloat(n)));
+
+        // Automatically convert percentage into number
+        if (processPercent) {
+            n = parseInt(n * max, 10) / 100;
+        }
+
+        // Handle floating point rounding errors
+        if ((math.abs(n - max) < 0.000001)) {
+            return 1;
+        }
+
+        // Convert into [0, 1] range if it isn't already
+        return (n % max) / parseFloat(max);
+    }
+
+    // Force a number between 0 and 1
+    function clamp01(val) {
+        return mathMin(1, mathMax(0, val));
+    }
+
+    // Parse an integer into hex
+    function parseHex(val) {
+        return parseInt(val, 16);
+    }
+
+    // Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
+    // <http://stackoverflow.com/questions/7422072/javascript-how-to-detect-number-as-a-decimal-including-1-0>
+    function isOnePointZero(n) {
+        return typeof n == "string" && n.indexOf('.') != -1 && parseFloat(n) === 1;
+    }
+
+    // Check to see if string passed in is a percentage
+    function isPercentage(n) {
+        return typeof n === "string" && n.indexOf('%') != -1;
+    }
+
+    // Force a hex value to have 2 characters
+    function pad2(c) {
+        return c.length == 1 ? '0' + c : '' + c;
+    }
+
+    // Replace a decimal with it's percentage value
+    function convertToPercentage(n) {
+        if (n <= 1) {
+            n = (n * 100) + "%";
+        }
+
+        return n;
+    }
+
+    var matchers = (function() {
+
+        // <http://www.w3.org/TR/css3-values/#integers>
+        var CSS_INTEGER = "[-\\+]?\\d+%?";
+
+        // <http://www.w3.org/TR/css3-values/#number-value>
+        var CSS_NUMBER = "[-\\+]?\\d*\\.\\d+%?";
+
+        // Allow positive/negative integer/number.  Don't capture the either/or, just the entire outcome.
+        var CSS_UNIT = "(?:" + CSS_NUMBER + ")|(?:" + CSS_INTEGER + ")";
+
+        // Actual matching.
+        // Parentheses and commas are optional, but not required.
+        // Whitespace can take the place of commas or opening paren
+        var PERMISSIVE_MATCH3 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?";
+        var PERMISSIVE_MATCH4 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?";
+
+        return {
+            rgb: new RegExp("rgb" + PERMISSIVE_MATCH3),
+            rgba: new RegExp("rgba" + PERMISSIVE_MATCH4),
+            hsl: new RegExp("hsl" + PERMISSIVE_MATCH3),
+            hsla: new RegExp("hsla" + PERMISSIVE_MATCH4),
+            hsv: new RegExp("hsv" + PERMISSIVE_MATCH3),
+            hex3: /^([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
+            hex6: /^([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/
+        };
+    })();
+
+    // `stringInputToObject`
+    // Permissive string parsing.  Take in a number of formats, and output an object
+    // based on detected format.  Returns `{ r, g, b }` or `{ h, s, l }` or `{ h, s, v}`
+    function stringInputToObject(color) {
+
+        color = color.replace(trimLeft,'').replace(trimRight, '').toLowerCase();
+        var named = false;
+        if (names[color]) {
+            color = names[color];
+            named = true;
+        }
+        else if (color == 'transparent') {
+            return { r: 0, g: 0, b: 0, a: 0, format: "name" };
+        }
+
+        // Try to match string input using regular expressions.
+        // Keep most of the number bounding out of this function - don't worry about [0,1] or [0,100] or [0,360]
+        // Just return an object and let the conversion functions handle that.
+        // This way the result will be the same whether the tinycolor is initialized with string or object.
+        var match;
+        if ((match = matchers.rgb.exec(color))) {
+            return { r: match[1], g: match[2], b: match[3] };
+        }
+        if ((match = matchers.rgba.exec(color))) {
+            return { r: match[1], g: match[2], b: match[3], a: match[4] };
+        }
+        if ((match = matchers.hsl.exec(color))) {
+            return { h: match[1], s: match[2], l: match[3] };
+        }
+        if ((match = matchers.hsla.exec(color))) {
+            return { h: match[1], s: match[2], l: match[3], a: match[4] };
+        }
+        if ((match = matchers.hsv.exec(color))) {
+            return { h: match[1], s: match[2], v: match[3] };
+        }
+        if ((match = matchers.hex6.exec(color))) {
+            return {
+                r: parseHex(match[1]),
+                g: parseHex(match[2]),
+                b: parseHex(match[3]),
+                format: named ? "name" : "hex"
+            };
+        }
+        if ((match = matchers.hex3.exec(color))) {
+            return {
+                r: parseHex(match[1] + '' + match[1]),
+                g: parseHex(match[2] + '' + match[2]),
+                b: parseHex(match[3] + '' + match[3]),
+                format: named ? "name" : "hex"
+            };
+        }
+
+        return false;
+    }
+
+    // Node: Export function
+    if (typeof module !== "undefined" && module.exports) {
+        module.exports = tinycolor;
+    }
+    // AMD/requirejs: Define the module
+    else if (typeof define !== "undefined") {
+        define(function () {return tinycolor;});
+    }
+    // Browser: Expose to window
+    else {
+        window.tinycolor = tinycolor;
+    }
+
+    })();
+
+
+    $(function () {
+        if ($.fn.spectrum.load) {
+            $.fn.spectrum.processNativeColorInputs();
+        }
+    });
+
+})(window, jQuery);
Index: /extensions/EasyCaptcha/template/comment.tpl
===================================================================
--- /extensions/EasyCaptcha/template/comment.tpl	(revision 24215)
+++ /extensions/EasyCaptcha/template/comment.tpl	(revision 24215)
@@ -0,0 +1,12 @@
+{include file=$EASYCAPTCHA_ABS_PATH|cat:'template/common.inc.tpl'}
+
+{* <!-- DRAG & DROP --> *}
+{if $EASYCAPTCHA_CHALLENGE == 'drag'}
+<p><label>{'easycaptcha_drag_%s'|translate|sprintf:$EASYCAPTCHA_CONF.text}</label></p>
+
+{* <!-- TIC TAC TOE --> *}
+{else if $EASYCAPTCHA_CHALLENGE == 'tictac'}
+<p><label>{'easycaptcha_tictac'|translate}</label></p>
+
+{/if}
+{$smarty.capture.easycaptcha}
Index: /extensions/EasyCaptcha/template/common.inc.tpl
===================================================================
--- /extensions/EasyCaptcha/template/common.inc.tpl	(revision 24215)
+++ /extensions/EasyCaptcha/template/common.inc.tpl	(revision 24215)
@@ -0,0 +1,155 @@
+{* <!-- DRAG & DROP --> *}
+{if $EASYCAPTCHA_CHALLENGE == 'drag'}
+{combine_script id='jquery.event.drag' load='footer' require='jquery' path=$EASYCAPTCHA_PATH|cat:'template/jquery.events/jquery.event.drag-2.2.js'}
+{combine_script id='jquery.event.drop' load='footer' require='jquery' path=$EASYCAPTCHA_PATH|cat:'template/jquery.events/jquery.event.drop-2.2.js'}
+
+{combine_script id='easycaptcha.drag' load='footer' require='jquery.event.drag,jquery.event.drop' path=$EASYCAPTCHA_PATH|cat:'template/drag.js'}
+
+{html_style}
+#easycaptcha, #easycaptcha_noscript {ldelim}
+  display:inline-block;
+  position:relative;
+  padding:10px;
+  border-radius:8px;
+  background: {$EASYCAPTCHA_CONF.bg1};
+  background: -webkit-linear-gradient(top, {$EASYCAPTCHA_CONF.bg1} 0%, {$EASYCAPTCHA_CONF.bg2} 100%);
+  background: -moz-linear-gradient(top, {$EASYCAPTCHA_CONF.bg1} 0%, {$EASYCAPTCHA_CONF.bg2} 100%);
+  background: -ms-linear-gradient(top, {$EASYCAPTCHA_CONF.bg1} 0%, {$EASYCAPTCHA_CONF.bg2} 100%);
+  background: -o-linear-gradient(top, {$EASYCAPTCHA_CONF.bg1} 0%, {$EASYCAPTCHA_CONF.bg2} 100%);
+  background: linear-gradient(to bottom, {$EASYCAPTCHA_CONF.bg1} 0%, {$EASYCAPTCHA_CONF.bg2} 100%);
+  color:{$EASYCAPTCHA_CONF.txt};
+}
+#easycaptcha .drag_item {ldelim}
+  position:absolute;
+  top:15px;
+  width:{$EASYCAPTCHA_CONF.size}px;
+  height:{$EASYCAPTCHA_CONF.size}px;
+  border-radius:5px;
+  border:1px solid {$EASYCAPTCHA_CONF.bd1};
+  background:{$EASYCAPTCHA_CONF.obj};
+  z-index:10;
+  cursor:move;
+}
+#easycaptcha .drag_item.active {ldelim}
+  z-index:100;
+  opacity:0.75;
+}
+#easycaptcha .drag_item img {ldelim}
+  width:{$EASYCAPTCHA_CONF.size}px;
+  height:{$EASYCAPTCHA_CONF.size}px;
+  border-radius:5px;
+}
+#easycaptcha .drop_zone {ldelim}
+  -moz-box-sizing:borderbox;
+  box-sizing:borderbox;
+  padding:5px;
+  width:{$EASYCAPTCHA_CONF.size}px;
+  height:{$EASYCAPTCHA_CONF.size}px;
+  margin-left:{math equation='15+(x+5)*y' x=$EASYCAPTCHA_CONF.size y=$EASYCAPTCHA_CONF.nb}px;
+  line-height:{$EASYCAPTCHA_CONF.size}px;
+  background: {$EASYCAPTCHA_CONF.bg1};
+  background: -webkit-linear-gradient(bottom, {$EASYCAPTCHA_CONF.bg1} 0%, {$EASYCAPTCHA_CONF.bg2} 100%);
+  background: -moz-linear-gradient(bottom, {$EASYCAPTCHA_CONF.bg1} 0%, {$EASYCAPTCHA_CONF.bg2} 100%);
+  background: -ms-linear-gradient(bottom, {$EASYCAPTCHA_CONF.bg1} 0%, {$EASYCAPTCHA_CONF.bg2} 100%);
+  background: -o-linear-gradient(bottom, {$EASYCAPTCHA_CONF.bg1} 0%, {$EASYCAPTCHA_CONF.bg2} 100%);
+  background: linear-gradient(to top, {$EASYCAPTCHA_CONF.bg1} 0%, {$EASYCAPTCHA_CONF.bg2} 100%);
+  border:1px dotted {$EASYCAPTCHA_CONF.bd2};
+  border-radius:5px;
+  text-align:center;
+  text-transform:uppercase;
+  color:{$EASYCAPTCHA_CONF.txt};
+}
+#easycaptcha .drop_zone.active {ldelim}
+  background:{$EASYCAPTCHA_CONF.sel};
+}
+#easycaptcha .drop_zone.valid {ldelim}
+  background:{$EASYCAPTCHA_CONF.sel};
+  box-shadow:0 0 0 2px {$EASYCAPTCHA_CONF.sel};
+}
+{/html_style}
+
+{capture name=easycaptcha}
+<noscript id="easycaptcha_noscript">
+  {'easycaptcha_javascript_public'|translate}
+</noscript>
+
+<div id="easycaptcha" style="display:none;">
+{counter start=0 assign=i}
+{foreach from=$EASYCAPTCHA_CONF.selection item=image}
+  <div class="drag_item" style="left:{math equation='10+(x+5)*y' x=$EASYCAPTCHA_CONF.size y=$i}px;" data-id="{$image}">
+    <img src="{$ROOT_URL}{$EASYCAPTCHA_PATH}drag/get.php?{$EASYCAPTCHA_CONF.theme}&amp;{$image}">
+  </div>
+  {counter}
+{/foreach}
+  <div class="drop_zone">{'Drop'|translate}</div>
+</div>
+
+{* <!-- fields are not type "hidden" for LiveValidation in GuestBook and ContactForm --> *}
+<input type="text" name="easycaptcha" value="" style="display:none;">
+<input type="text" name="easycaptcha_key" value="{$EASYCAPTCHA_CONF.key}" style="display:none;">
+{/capture}
+
+
+{* <!-- TIC TAC TOE --> *}
+{else if $EASYCAPTCHA_CHALLENGE == 'tictac'}
+{html_style}
+#easycaptcha table {ldelim}
+  width:{$EASYCAPTCHA_CONF.size}px;
+  height:{$EASYCAPTCHA_CONF.size}px;
+  border-collapse:collapse;
+  display:inline-block;
+  margin:0;
+  background:url('{$ROOT_URL}{$EASYCAPTCHA_PATH}tictac/gen.php?t={$smarty.now}') no-repeat;
+}
+#easycaptcha td {ldelim}
+  border:none;
+  padding:0;
+}
+#easycaptcha label {ldelim}
+  display:block;
+  width:{math equation="floor(x/3)" x=$EASYCAPTCHA_CONF.size}px;
+  height:{math equation="floor(x/3)" x=$EASYCAPTCHA_CONF.size}px;
+  cursor:url('{$ROOT_URL}{$EASYCAPTCHA_PATH}tictac/gen.php?cross=96') 16 16, pointer;
+}
+#easycaptcha input {ldelim}
+  display:none;
+}
+#easycaptcha label.selected {ldelim}
+  background:url('{$ROOT_URL}{$EASYCAPTCHA_PATH}tictac/gen.php?cross={$EASYCAPTCHA_CONF.size}') no-repeat;
+}
+{/html_style}
+
+{footer_script require='jquery'}{literal}
+(function($){
+$('#easycaptcha input').on('change', function() {
+    $('#easycaptcha label').removeClass('selected');
+    $(this).parent('label').addClass('selected');
+});
+}(jQuery));
+{/literal}{/footer_script}
+
+{capture name=easycaptcha}
+<div id="easycaptcha">
+  <table>
+    <tr>
+      <td><label><input type="radio" name="easycaptcha" value="00"></label></td>
+      <td><label><input type="radio" name="easycaptcha" value="10"></label></td>
+      <td><label><input type="radio" name="easycaptcha" value="20"></label></td>
+    </tr>
+    <tr>
+      <td><label><input type="radio" name="easycaptcha" value="01"></label></td>
+      <td><label><input type="radio" name="easycaptcha" value="11"></label></td>
+      <td><label><input type="radio" name="easycaptcha" value="21"></label></td>
+    </tr>
+    <tr>
+      <td><label><input type="radio" name="easycaptcha" value="02"></label></td>
+      <td><label><input type="radio" name="easycaptcha" value="12"></label></td>
+      <td><label><input type="radio" name="easycaptcha" value="22"></label></td>
+    </tr>
+  </table>
+</div>
+
+<input type="text" name="easycaptcha_key" value="{$EASYCAPTCHA_CONF.key}" style="display:none;">
+{/capture}
+
+{/if}
Index: /extensions/EasyCaptcha/template/contactform.tpl
===================================================================
--- /extensions/EasyCaptcha/template/contactform.tpl	(revision 24215)
+++ /extensions/EasyCaptcha/template/contactform.tpl	(revision 24215)
@@ -0,0 +1,31 @@
+</tr>
+<tr>
+  {include file=$EASYCAPTCHA_ABS_PATH|cat:'template/common.inc.tpl'}
+
+  {* <!-- DRAG & DROP --> *}
+  {if $EASYCAPTCHA_CHALLENGE == 'drag'}
+  <td class="title">{'easycaptcha_drag_%s'|translate|sprintf:$EASYCAPTCHA_CONF.text}</td>
+
+  {footer_script}
+  var captcha_code = new LiveValidation(jQuery('input[name="easycaptcha"]')[0], {ldelim} onlyOnSubmit: true });
+  captcha_code.add(Validate.Presence, {ldelim} failureMessage: "{'Pleaser answer'|translate}" });
+  {/footer_script}
+
+  {* <!-- TIC TAC TOE --> *}
+  {else if $EASYCAPTCHA_CHALLENGE == 'tictac'}
+  <td class="title">{'easycaptcha_tictac'|translate}</td>
+
+  {footer_script}
+  var captcha_code = new LiveValidation(jQuery('input[name="easycaptcha_key"]')[0], {ldelim} onlyOnSubmit: true });
+  captcha_code.add(Validate.Custom, {ldelim}
+    failureMessage: "{'Pleaser answer'|translate}",
+    against: function() {ldelim}
+        return jQuery('input[name="easycaptcha"]:checked').length != 0;
+    }
+  });
+  {/footer_script}
+
+  {/if}
+  <td>
+    {$smarty.capture.easycaptcha}
+  </td>
Index: /extensions/EasyCaptcha/template/drag.js
===================================================================
--- /extensions/EasyCaptcha/template/drag.js	(revision 24215)
+++ /extensions/EasyCaptcha/template/drag.js	(revision 24215)
@@ -0,0 +1,57 @@
+(function($){
+
+$.drop({ mode:true });
+Math.bounds = function(a, b, c) { return Math.max(a, Math.min(b, c)); }
+
+var $easycaptcha = $('#easycaptcha').show();
+
+$easycaptcha.find('.drag_item')
+  .each(function() {
+      $(this).data({
+          origTop: $(this).css('top'),
+          origLeft: $(this).css('left'),
+      });
+  })
+  .drag('start', function(e, dd) {
+      $(this).addClass('active');
+
+      dd.limit = {
+          top: 0,
+          left: 0,
+          bottom: $easycaptcha.outerHeight() - $(this).outerHeight(),
+          right: $easycaptcha.outerWidth() - $(this).outerWidth()
+      };
+
+      $('.drag_item').not(this).each(function() {
+          $(this).animate({
+              top: $(this).data('origTop'),
+              left: $(this).data('origLeft')
+          });
+      });
+
+      $('input[name="easycaptcha"]').val('');
+      $easycaptcha.find('.drop_zone').removeClass('valid');
+  })
+  .drag(function(e, dd) {
+      $(this).css({
+          top: Math.bounds(dd.limit.top, dd.offsetY - $easycaptcha.offset().top, dd.limit.bottom),
+          left: Math.bounds(dd.limit.left, dd.offsetX - $easycaptcha.offset().left, dd.limit.right)
+      });
+  })
+  .drag('end', function(e, dd) {
+      $(this).removeClass('active');
+  });
+
+$easycaptcha.find('.drop_zone')
+  .drop('start', function(e, dd) {
+      $(this).addClass('active');
+  })
+  .drop(function(e, dd) {
+      $('input[name="easycaptcha"]').val($(dd.drag).data('id'));
+      $(this).addClass('valid');
+  })
+  .drop('end', function() {
+      $(this).removeClass('active');
+  });
+
+}(jQuery));
Index: /extensions/EasyCaptcha/template/guestbook.tpl
===================================================================
--- /extensions/EasyCaptcha/template/guestbook.tpl	(revision 24215)
+++ /extensions/EasyCaptcha/template/guestbook.tpl	(revision 24215)
@@ -0,0 +1,33 @@
+<tr>
+  {include file=$EASYCAPTCHA_ABS_PATH|cat:'template/common.inc.tpl'}
+
+  {* <!-- DRAG & DROP --> *}
+  {if $EASYCAPTCHA_CHALLENGE == 'drag'}
+  <td colspan=2><label>{'easycaptcha_drag_%s'|translate|sprintf:$EASYCAPTCHA_CONF.text}</label></td>
+
+  {footer_script}
+  var captcha_code = new LiveValidation(jQuery('input[name="easycaptcha"]')[0], {ldelim} onlyOnSubmit: true });
+  captcha_code.add(Validate.Presence, {ldelim} failureMessage: "{'Pleaser answer'|translate}" });
+  {/footer_script}
+
+  {* <!-- TIC TAC TOE --> *}
+  {else if $EASYCAPTCHA_CHALLENGE == 'tictac'}
+  <td colspan=2><label>{'easycaptcha_tictac'|translate}</label></td>
+
+  {footer_script}
+  var captcha_code = new LiveValidation(jQuery('input[name="easycaptcha_key"]')[0], {ldelim} onlyOnSubmit: true });
+  captcha_code.add(Validate.Custom, {ldelim}
+    failureMessage: "{'Pleaser answer'|translate}",
+    against: function() {ldelim}
+        return jQuery('input[name="easycaptcha"]:checked').length != 0;
+    }
+  });
+  {/footer_script}
+
+  {/if}
+</tr>
+<tr>
+  <td colspan=2 style="text-align:center;">
+    {$smarty.capture.easycaptcha}
+  </td>
+</tr>
Index: /extensions/EasyCaptcha/template/index.php
===================================================================
--- /extensions/EasyCaptcha/template/index.php	(revision 24215)
+++ /extensions/EasyCaptcha/template/index.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
Index: /extensions/EasyCaptcha/template/jquery.events/jquery.event.drag-2.2.js
===================================================================
--- /extensions/EasyCaptcha/template/jquery.events/jquery.event.drag-2.2.js	(revision 24215)
+++ /extensions/EasyCaptcha/template/jquery.events/jquery.event.drag-2.2.js	(revision 24215)
@@ -0,0 +1,402 @@
+/*! 
+ * jquery.event.drag - v 2.2
+ * Copyright (c) 2010 Three Dub Media - http://threedubmedia.com
+ * Open Source MIT License - http://threedubmedia.com/code/license
+ */
+// Created: 2008-06-04 
+// Updated: 2012-05-21
+// REQUIRES: jquery 1.7.x
+
+;(function( $ ){
+
+// add the jquery instance method
+$.fn.drag = function( str, arg, opts ){
+	// figure out the event type
+	var type = typeof str == "string" ? str : "",
+	// figure out the event handler...
+	fn = $.isFunction( str ) ? str : $.isFunction( arg ) ? arg : null;
+	// fix the event type
+	if ( type.indexOf("drag") !== 0 ) 
+		type = "drag"+ type;
+	// were options passed
+	opts = ( str == fn ? arg : opts ) || {};
+	// trigger or bind event handler
+	return fn ? this.bind( type, opts, fn ) : this.trigger( type );
+};
+
+// local refs (increase compression)
+var $event = $.event, 
+$special = $event.special,
+// configure the drag special event 
+drag = $special.drag = {
+	
+	// these are the default settings
+	defaults: {
+		which: 1, // mouse button pressed to start drag sequence
+		distance: 0, // distance dragged before dragstart
+		not: ':input', // selector to suppress dragging on target elements
+		handle: null, // selector to match handle target elements
+		relative: false, // true to use "position", false to use "offset"
+		drop: true, // false to suppress drop events, true or selector to allow
+		click: false // false to suppress click events after dragend (no proxy)
+	},
+	
+	// the key name for stored drag data
+	datakey: "dragdata",
+	
+	// prevent bubbling for better performance
+	noBubble: true,
+	
+	// count bound related events
+	add: function( obj ){ 
+		// read the interaction data
+		var data = $.data( this, drag.datakey ),
+		// read any passed options 
+		opts = obj.data || {};
+		// count another realted event
+		data.related += 1;
+		// extend data options bound with this event
+		// don't iterate "opts" in case it is a node 
+		$.each( drag.defaults, function( key, def ){
+			if ( opts[ key ] !== undefined )
+				data[ key ] = opts[ key ];
+		});
+	},
+	
+	// forget unbound related events
+	remove: function(){
+		$.data( this, drag.datakey ).related -= 1;
+	},
+	
+	// configure interaction, capture settings
+	setup: function(){
+		// check for related events
+		if ( $.data( this, drag.datakey ) ) 
+			return;
+		// initialize the drag data with copied defaults
+		var data = $.extend({ related:0 }, drag.defaults );
+		// store the interaction data
+		$.data( this, drag.datakey, data );
+		// bind the mousedown event, which starts drag interactions
+		$event.add( this, "touchstart mousedown", drag.init, data );
+		// prevent image dragging in IE...
+		if ( this.attachEvent ) 
+			this.attachEvent("ondragstart", drag.dontstart ); 
+	},
+	
+	// destroy configured interaction
+	teardown: function(){
+		var data = $.data( this, drag.datakey ) || {};
+		// check for related events
+		if ( data.related ) 
+			return;
+		// remove the stored data
+		$.removeData( this, drag.datakey );
+		// remove the mousedown event
+		$event.remove( this, "touchstart mousedown", drag.init );
+		// enable text selection
+		drag.textselect( true ); 
+		// un-prevent image dragging in IE...
+		if ( this.detachEvent ) 
+			this.detachEvent("ondragstart", drag.dontstart ); 
+	},
+		
+	// initialize the interaction
+	init: function( event ){ 
+		// sorry, only one touch at a time
+		if ( drag.touched ) 
+			return;
+		// the drag/drop interaction data
+		var dd = event.data, results;
+		// check the which directive
+		if ( event.which != 0 && dd.which > 0 && event.which != dd.which ) 
+			return; 
+		// check for suppressed selector
+		if ( $( event.target ).is( dd.not ) ) 
+			return;
+		// check for handle selector
+		if ( dd.handle && !$( event.target ).closest( dd.handle, event.currentTarget ).length ) 
+			return;
+
+		drag.touched = event.type == 'touchstart' ? this : null;
+		dd.propagates = 1;
+		dd.mousedown = this;
+		dd.interactions = [ drag.interaction( this, dd ) ];
+		dd.target = event.target;
+		dd.pageX = event.pageX;
+		dd.pageY = event.pageY;
+		dd.dragging = null;
+		// handle draginit event... 
+		results = drag.hijack( event, "draginit", dd );
+		// early cancel
+		if ( !dd.propagates )
+			return;
+		// flatten the result set
+		results = drag.flatten( results );
+		// insert new interaction elements
+		if ( results && results.length ){
+			dd.interactions = [];
+			$.each( results, function(){
+				dd.interactions.push( drag.interaction( this, dd ) );
+			});
+		}
+		// remember how many interactions are propagating
+		dd.propagates = dd.interactions.length;
+		// locate and init the drop targets
+		if ( dd.drop !== false && $special.drop ) 
+			$special.drop.handler( event, dd );
+		// disable text selection
+		drag.textselect( false ); 
+		// bind additional events...
+		if ( drag.touched )
+			$event.add( drag.touched, "touchmove touchend", drag.handler, dd );
+		else 
+			$event.add( document, "mousemove mouseup", drag.handler, dd );
+		// helps prevent text selection or scrolling
+		if ( !drag.touched || dd.live )
+			return false;
+	},	
+	
+	// returns an interaction object
+	interaction: function( elem, dd ){
+		var offset = $( elem )[ dd.relative ? "position" : "offset" ]() || { top:0, left:0 };
+		return {
+			drag: elem, 
+			callback: new drag.callback(), 
+			droppable: [],
+			offset: offset
+		};
+	},
+	
+	// handle drag-releatd DOM events
+	handler: function( event ){ 
+		// read the data before hijacking anything
+		var dd = event.data;	
+		// handle various events
+		switch ( event.type ){
+			// mousemove, check distance, start dragging
+			case !dd.dragging && 'touchmove': 
+				event.preventDefault();
+			case !dd.dragging && 'mousemove':
+				//  drag tolerance, x² + y² = distance²
+				if ( Math.pow(  event.pageX-dd.pageX, 2 ) + Math.pow(  event.pageY-dd.pageY, 2 ) < Math.pow( dd.distance, 2 ) ) 
+					break; // distance tolerance not reached
+				event.target = dd.target; // force target from "mousedown" event (fix distance issue)
+				drag.hijack( event, "dragstart", dd ); // trigger "dragstart"
+				if ( dd.propagates ) // "dragstart" not rejected
+					dd.dragging = true; // activate interaction
+			// mousemove, dragging
+			case 'touchmove':
+				event.preventDefault();
+			case 'mousemove':
+				if ( dd.dragging ){
+					// trigger "drag"		
+					drag.hijack( event, "drag", dd );
+					if ( dd.propagates ){
+						// manage drop events
+						if ( dd.drop !== false && $special.drop )
+							$special.drop.handler( event, dd ); // "dropstart", "dropend"							
+						break; // "drag" not rejected, stop		
+					}
+					event.type = "mouseup"; // helps "drop" handler behave
+				}
+			// mouseup, stop dragging
+			case 'touchend': 
+			case 'mouseup': 
+			default:
+				if ( drag.touched )
+					$event.remove( drag.touched, "touchmove touchend", drag.handler ); // remove touch events
+				else 
+					$event.remove( document, "mousemove mouseup", drag.handler ); // remove page events	
+				if ( dd.dragging ){
+					if ( dd.drop !== false && $special.drop )
+						$special.drop.handler( event, dd ); // "drop"
+					drag.hijack( event, "dragend", dd ); // trigger "dragend"	
+				}
+				drag.textselect( true ); // enable text selection
+				// if suppressing click events...
+				if ( dd.click === false && dd.dragging )
+					$.data( dd.mousedown, "suppress.click", new Date().getTime() + 5 );
+				dd.dragging = drag.touched = false; // deactivate element	
+				break;
+		}
+	},
+		
+	// re-use event object for custom events
+	hijack: function( event, type, dd, x, elem ){
+		// not configured
+		if ( !dd ) 
+			return;
+		// remember the original event and type
+		var orig = { event:event.originalEvent, type:event.type },
+		// is the event drag related or drog related?
+		mode = type.indexOf("drop") ? "drag" : "drop",
+		// iteration vars
+		result, i = x || 0, ia, $elems, callback,
+		len = !isNaN( x ) ? x : dd.interactions.length;
+		// modify the event type
+		event.type = type;
+		// remove the original event
+		event.originalEvent = null;
+		// initialize the results
+		dd.results = [];
+		// handle each interacted element
+		do if ( ia = dd.interactions[ i ] ){
+			// validate the interaction
+			if ( type !== "dragend" && ia.cancelled )
+				continue;
+			// set the dragdrop properties on the event object
+			callback = drag.properties( event, dd, ia );
+			// prepare for more results
+			ia.results = [];
+			// handle each element
+			$( elem || ia[ mode ] || dd.droppable ).each(function( p, subject ){
+				// identify drag or drop targets individually
+				callback.target = subject;
+				// force propagtion of the custom event
+				event.isPropagationStopped = function(){ return false; };
+				// handle the event	
+				result = subject ? $event.dispatch.call( subject, event, callback ) : null;
+				// stop the drag interaction for this element
+				if ( result === false ){
+					if ( mode == "drag" ){
+						ia.cancelled = true;
+						dd.propagates -= 1;
+					}
+					if ( type == "drop" ){
+						ia[ mode ][p] = null;
+					}
+				}
+				// assign any dropinit elements
+				else if ( type == "dropinit" )
+					ia.droppable.push( drag.element( result ) || subject );
+				// accept a returned proxy element 
+				if ( type == "dragstart" )
+					ia.proxy = $( drag.element( result ) || ia.drag )[0];
+				// remember this result	
+				ia.results.push( result );
+				// forget the event result, for recycling
+				delete event.result;
+				// break on cancelled handler
+				if ( type !== "dropinit" )
+					return result;
+			});	
+			// flatten the results	
+			dd.results[ i ] = drag.flatten( ia.results );	
+			// accept a set of valid drop targets
+			if ( type == "dropinit" )
+				ia.droppable = drag.flatten( ia.droppable );
+			// locate drop targets
+			if ( type == "dragstart" && !ia.cancelled )
+				callback.update(); 
+		}
+		while ( ++i < len )
+		// restore the original event & type
+		event.type = orig.type;
+		event.originalEvent = orig.event;
+		// return all handler results
+		return drag.flatten( dd.results );
+	},
+		
+	// extend the callback object with drag/drop properties...
+	properties: function( event, dd, ia ){		
+		var obj = ia.callback;
+		// elements
+		obj.drag = ia.drag;
+		obj.proxy = ia.proxy || ia.drag;
+		// starting mouse position
+		obj.startX = dd.pageX;
+		obj.startY = dd.pageY;
+		// current distance dragged
+		obj.deltaX = event.pageX - dd.pageX;
+		obj.deltaY = event.pageY - dd.pageY;
+		// original element position
+		obj.originalX = ia.offset.left;
+		obj.originalY = ia.offset.top;
+		// adjusted element position
+		obj.offsetX = obj.originalX + obj.deltaX; 
+		obj.offsetY = obj.originalY + obj.deltaY;
+		// assign the drop targets information
+		obj.drop = drag.flatten( ( ia.drop || [] ).slice() );
+		obj.available = drag.flatten( ( ia.droppable || [] ).slice() );
+		return obj;	
+	},
+	
+	// determine is the argument is an element or jquery instance
+	element: function( arg ){
+		if ( arg && ( arg.jquery || arg.nodeType == 1 ) )
+			return arg;
+	},
+	
+	// flatten nested jquery objects and arrays into a single dimension array
+	flatten: function( arr ){
+		return $.map( arr, function( member ){
+			return member && member.jquery ? $.makeArray( member ) : 
+				member && member.length ? drag.flatten( member ) : member;
+		});
+	},
+	
+	// toggles text selection attributes ON (true) or OFF (false)
+	textselect: function( bool ){ 
+		$( document )[ bool ? "unbind" : "bind" ]("selectstart", drag.dontstart )
+			.css("MozUserSelect", bool ? "" : "none" );
+		// .attr("unselectable", bool ? "off" : "on" )
+		document.unselectable = bool ? "off" : "on"; 
+	},
+	
+	// suppress "selectstart" and "ondragstart" events
+	dontstart: function(){ 
+		return false; 
+	},
+	
+	// a callback instance contructor
+	callback: function(){}
+	
+};
+
+// callback methods
+drag.callback.prototype = {
+	update: function(){
+		if ( $special.drop && this.available.length )
+			$.each( this.available, function( i ){
+				$special.drop.locate( this, i );
+			});
+	}
+};
+
+// patch $.event.$dispatch to allow suppressing clicks
+var $dispatch = $event.dispatch;
+$event.dispatch = function( event ){
+	if ( $.data( this, "suppress."+ event.type ) - new Date().getTime() > 0 ){
+		$.removeData( this, "suppress."+ event.type );
+		return;
+	}
+	return $dispatch.apply( this, arguments );
+};
+
+// event fix hooks for touch events...
+var touchHooks = 
+$event.fixHooks.touchstart = 
+$event.fixHooks.touchmove = 
+$event.fixHooks.touchend =
+$event.fixHooks.touchcancel = {
+	props: "clientX clientY pageX pageY screenX screenY".split( " " ),
+	filter: function( event, orig ) {
+		if ( orig ){
+			var touched = ( orig.touches && orig.touches[0] )
+				|| ( orig.changedTouches && orig.changedTouches[0] )
+				|| null; 
+			// iOS webkit: touchstart, touchmove, touchend
+			if ( touched ) 
+				$.each( touchHooks.props, function( i, prop ){
+					event[ prop ] = touched[ prop ];
+				});
+		}
+		return event;
+	}
+};
+
+// share the same special event configuration with related events...
+$special.draginit = $special.dragstart = $special.dragend = drag;
+
+})( jQuery );
Index: /extensions/EasyCaptcha/template/jquery.events/jquery.event.drag.live-2.2.js
===================================================================
--- /extensions/EasyCaptcha/template/jquery.events/jquery.event.drag.live-2.2.js	(revision 24215)
+++ /extensions/EasyCaptcha/template/jquery.events/jquery.event.drag.live-2.2.js	(revision 24215)
@@ -0,0 +1,87 @@
+/*! 
+ * jquery.event.drag.live - v 2.2
+ * Copyright (c) 2010 Three Dub Media - http://threedubmedia.com
+ * Open Source MIT License - http://threedubmedia.com/code/license
+ */
+// Created: 2010-06-07
+// Updated: 2012-05-21
+// REQUIRES: jquery 1.7.x, event.drag 2.2
+
+;(function( $ ){
+	
+// local refs (increase compression)
+var $event = $.event,
+// ref the special event config
+drag = $event.special.drag,
+// old drag event add method
+origadd = drag.add,
+// old drag event teradown method
+origteardown = drag.teardown;
+
+// allow events to bubble for delegation
+drag.noBubble = false;
+
+// the namespace for internal live events
+drag.livekey = "livedrag";
+
+// new drop event add method
+drag.add = function( obj ){ 
+	// call the old method
+	origadd.apply( this, arguments );
+	// read the data
+	var data = $.data( this, drag.datakey );
+	// bind the live "draginit" delegator
+	if ( !data.live && obj.selector ){
+		data.live = true;
+		$event.add( this, "draginit."+ drag.livekey, drag.delegate );
+	}
+};
+
+// new drop event teardown method
+drag.teardown = function(){ 
+	// call the old method
+	origteardown.apply( this, arguments );
+	// read the data
+	var data = $.data( this, drag.datakey ) || {};
+	// bind the live "draginit" delegator
+	if ( data.live ){
+		// remove the "live" delegation
+		$event.remove( this, "draginit."+ drag.livekey, drag.delegate );
+		data.live = false;
+	}
+};
+
+// identify potential delegate elements
+drag.delegate = function( event ){
+	// local refs
+	var elems = [], target, 
+	// element event structure
+	events = $.data( this, "events" ) || {};
+	// query live events
+	$.each( events || [], function( key, arr ){
+		// no event type matches
+		if ( key.indexOf("drag") !== 0 )
+			return;
+		$.each( arr || [], function( i, obj ){
+			// locate the element to delegate
+			target = $( event.target ).closest( obj.selector, event.currentTarget )[0];
+			// no element found
+			if ( !target ) 
+				return;
+			// add an event handler
+			$event.add( target, obj.origType+'.'+drag.livekey, obj.origHandler || obj.handler, obj.data );
+			// remember new elements
+			if ( $.inArray( target, elems ) < 0 )
+				elems.push( target );		
+		});
+	});
+	// if there are no elements, break
+	if ( !elems.length ) 
+		return false;
+	// return the matched results, and clenup when complete		
+	return $( elems ).bind("dragend."+ drag.livekey, function(){
+		$event.remove( this, "."+ drag.livekey ); // cleanup delegation
+	});
+};
+	
+})( jQuery );
Index: /extensions/EasyCaptcha/template/jquery.events/jquery.event.drop-2.2.js
===================================================================
--- /extensions/EasyCaptcha/template/jquery.events/jquery.event.drop-2.2.js	(revision 24215)
+++ /extensions/EasyCaptcha/template/jquery.events/jquery.event.drop-2.2.js	(revision 24215)
@@ -0,0 +1,302 @@
+/*! 
+ * jquery.event.drop - v 2.2
+ * Copyright (c) 2010 Three Dub Media - http://threedubmedia.com
+ * Open Source MIT License - http://threedubmedia.com/code/license
+ */
+// Created: 2008-06-04 
+// Updated: 2012-05-21
+// REQUIRES: jquery 1.7.x, event.drag 2.2
+
+;(function($){ // secure $ jQuery alias
+
+// Events: drop, dropstart, dropend
+
+// add the jquery instance method
+$.fn.drop = function( str, arg, opts ){
+	// figure out the event type
+	var type = typeof str == "string" ? str : "",
+	// figure out the event handler...
+	fn = $.isFunction( str ) ? str : $.isFunction( arg ) ? arg : null;
+	// fix the event type
+	if ( type.indexOf("drop") !== 0 ) 
+		type = "drop"+ type;
+	// were options passed
+	opts = ( str == fn ? arg : opts ) || {};
+	// trigger or bind event handler
+	return fn ? this.bind( type, opts, fn ) : this.trigger( type );
+};
+
+// DROP MANAGEMENT UTILITY
+// returns filtered drop target elements, caches their positions
+$.drop = function( opts ){ 
+	opts = opts || {};
+	// safely set new options...
+	drop.multi = opts.multi === true ? Infinity : 
+		opts.multi === false ? 1 : !isNaN( opts.multi ) ? opts.multi : drop.multi;
+	drop.delay = opts.delay || drop.delay;
+	drop.tolerance = $.isFunction( opts.tolerance ) ? opts.tolerance : 
+		opts.tolerance === null ? null : drop.tolerance;
+	drop.mode = opts.mode || drop.mode || 'intersect';
+};
+
+// local refs (increase compression)
+var $event = $.event, 
+$special = $event.special,
+// configure the drop special event
+drop = $.event.special.drop = {
+
+	// these are the default settings
+	multi: 1, // allow multiple drop winners per dragged element
+	delay: 20, // async timeout delay
+	mode: 'overlap', // drop tolerance mode
+		
+	// internal cache
+	targets: [], 
+	
+	// the key name for stored drop data
+	datakey: "dropdata",
+		
+	// prevent bubbling for better performance
+	noBubble: true,
+	
+	// count bound related events
+	add: function( obj ){ 
+		// read the interaction data
+		var data = $.data( this, drop.datakey );
+		// count another realted event
+		data.related += 1;
+	},
+	
+	// forget unbound related events
+	remove: function(){
+		$.data( this, drop.datakey ).related -= 1;
+	},
+	
+	// configure the interactions
+	setup: function(){
+		// check for related events
+		if ( $.data( this, drop.datakey ) ) 
+			return;
+		// initialize the drop element data
+		var data = { 
+			related: 0,
+			active: [],
+			anyactive: 0,
+			winner: 0,
+			location: {}
+		};
+		// store the drop data on the element
+		$.data( this, drop.datakey, data );
+		// store the drop target in internal cache
+		drop.targets.push( this );
+	},
+	
+	// destroy the configure interaction	
+	teardown: function(){ 
+		var data = $.data( this, drop.datakey ) || {};
+		// check for related events
+		if ( data.related ) 
+			return;
+		// remove the stored data
+		$.removeData( this, drop.datakey );
+		// reference the targeted element
+		var element = this;
+		// remove from the internal cache
+		drop.targets = $.grep( drop.targets, function( target ){ 
+			return ( target !== element ); 
+		});
+	},
+	
+	// shared event handler
+	handler: function( event, dd ){ 
+		// local vars
+		var results, $targets;
+		// make sure the right data is available
+		if ( !dd ) 
+			return;
+		// handle various events
+		switch ( event.type ){
+			// draginit, from $.event.special.drag
+			case 'mousedown': // DROPINIT >>
+			case 'touchstart': // DROPINIT >>
+				// collect and assign the drop targets
+				$targets =  $( drop.targets );
+				if ( typeof dd.drop == "string" )
+					$targets = $targets.filter( dd.drop );
+				// reset drop data winner properties
+				$targets.each(function(){
+					var data = $.data( this, drop.datakey );
+					data.active = [];
+					data.anyactive = 0;
+					data.winner = 0;
+				});
+				// set available target elements
+				dd.droppable = $targets;
+				// activate drop targets for the initial element being dragged
+				$special.drag.hijack( event, "dropinit", dd ); 
+				break;
+			// drag, from $.event.special.drag
+			case 'mousemove': // TOLERATE >>
+			case 'touchmove': // TOLERATE >>
+				drop.event = event; // store the mousemove event
+				if ( !drop.timer )
+					// monitor drop targets
+					drop.tolerate( dd ); 
+				break;
+			// dragend, from $.event.special.drag
+			case 'mouseup': // DROP >> DROPEND >>
+			case 'touchend': // DROP >> DROPEND >>
+				drop.timer = clearTimeout( drop.timer ); // delete timer	
+				if ( dd.propagates ){
+					$special.drag.hijack( event, "drop", dd ); 
+					$special.drag.hijack( event, "dropend", dd ); 
+				}
+				break;
+				
+		}
+	},
+		
+	// returns the location positions of an element
+	locate: function( elem, index ){ 
+		var data = $.data( elem, drop.datakey ),
+		$elem = $( elem ), 
+		posi = $elem.offset() || {}, 
+		height = $elem.outerHeight(), 
+		width = $elem.outerWidth(),
+		location = { 
+			elem: elem, 
+			width: width, 
+			height: height,
+			top: posi.top, 
+			left: posi.left, 
+			right: posi.left + width, 
+			bottom: posi.top + height
+		};
+		// drag elements might not have dropdata
+		if ( data ){
+			data.location = location;
+			data.index = index;
+			data.elem = elem;
+		}
+		return location;
+	},
+	
+	// test the location positions of an element against another OR an X,Y coord
+	contains: function( target, test ){ // target { location } contains test [x,y] or { location }
+		return ( ( test[0] || test.left ) >= target.left && ( test[0] || test.right ) <= target.right
+			&& ( test[1] || test.top ) >= target.top && ( test[1] || test.bottom ) <= target.bottom ); 
+	},
+	
+	// stored tolerance modes
+	modes: { // fn scope: "$.event.special.drop" object 
+		// target with mouse wins, else target with most overlap wins
+		'intersect': function( event, proxy, target ){
+			return this.contains( target, [ event.pageX, event.pageY ] ) ? // check cursor
+				1e9 : this.modes.overlap.apply( this, arguments ); // check overlap
+		},
+		// target with most overlap wins	
+		'overlap': function( event, proxy, target ){
+			// calculate the area of overlap...
+			return Math.max( 0, Math.min( target.bottom, proxy.bottom ) - Math.max( target.top, proxy.top ) )
+				* Math.max( 0, Math.min( target.right, proxy.right ) - Math.max( target.left, proxy.left ) );
+		},
+		// proxy is completely contained within target bounds	
+		'fit': function( event, proxy, target ){
+			return this.contains( target, proxy ) ? 1 : 0;
+		},
+		// center of the proxy is contained within target bounds	
+		'middle': function( event, proxy, target ){
+			return this.contains( target, [ proxy.left + proxy.width * .5, proxy.top + proxy.height * .5 ] ) ? 1 : 0;
+		}
+	},	
+	
+	// sort drop target cache by by winner (dsc), then index (asc)
+	sort: function( a, b ){
+		return ( b.winner - a.winner ) || ( a.index - b.index );
+	},
+		
+	// async, recursive tolerance execution
+	tolerate: function( dd ){		
+		// declare local refs
+		var i, drp, drg, data, arr, len, elem,
+		// interaction iteration variables
+		x = 0, ia, end = dd.interactions.length,
+		// determine the mouse coords
+		xy = [ drop.event.pageX, drop.event.pageY ],
+		// custom or stored tolerance fn
+		tolerance = drop.tolerance || drop.modes[ drop.mode ];
+		// go through each passed interaction...
+		do if ( ia = dd.interactions[x] ){
+			// check valid interaction
+			if ( !ia )
+				return; 
+			// initialize or clear the drop data
+			ia.drop = [];
+			// holds the drop elements
+			arr = []; 
+			len = ia.droppable.length;
+			// determine the proxy location, if needed
+			if ( tolerance )
+				drg = drop.locate( ia.proxy ); 
+			// reset the loop
+			i = 0;
+			// loop each stored drop target
+			do if ( elem = ia.droppable[i] ){ 
+				data = $.data( elem, drop.datakey );
+				drp = data.location;
+				if ( !drp ) continue;
+				// find a winner: tolerance function is defined, call it
+				data.winner = tolerance ? tolerance.call( drop, drop.event, drg, drp ) 
+					// mouse position is always the fallback
+					: drop.contains( drp, xy ) ? 1 : 0; 
+				arr.push( data );	
+			} while ( ++i < len ); // loop 
+			// sort the drop targets
+			arr.sort( drop.sort );			
+			// reset the loop
+			i = 0;
+			// loop through all of the targets again
+			do if ( data = arr[ i ] ){
+				// winners...
+				if ( data.winner && ia.drop.length < drop.multi ){
+					// new winner... dropstart
+					if ( !data.active[x] && !data.anyactive ){
+						// check to make sure that this is not prevented
+						if ( $special.drag.hijack( drop.event, "dropstart", dd, x, data.elem )[0] !== false ){ 	
+							data.active[x] = 1;
+							data.anyactive += 1;
+						}
+						// if false, it is not a winner
+						else
+							data.winner = 0;
+					}
+					// if it is still a winner
+					if ( data.winner )
+						ia.drop.push( data.elem );
+				}
+				// losers... 
+				else if ( data.active[x] && data.anyactive == 1 ){
+					// former winner... dropend
+					$special.drag.hijack( drop.event, "dropend", dd, x, data.elem ); 
+					data.active[x] = 0;
+					data.anyactive -= 1;
+				}
+			} while ( ++i < len ); // loop 		
+		} while ( ++x < end ) // loop
+		// check if the mouse is still moving or is idle
+		if ( drop.last && xy[0] == drop.last.pageX && xy[1] == drop.last.pageY ) 
+			delete drop.timer; // idle, don't recurse
+		else  // recurse
+			drop.timer = setTimeout(function(){ 
+				drop.tolerate( dd ); 
+			}, drop.delay );
+		// remember event, to compare idleness
+		drop.last = drop.event; 
+	}
+	
+};
+
+// share the same special event configuration with related events...
+$special.dropinit = $special.dropstart = $special.dropend = drop;
+
+})(jQuery);
Index: /extensions/EasyCaptcha/template/jquery.events/jquery.event.drop.live-2.2.js
===================================================================
--- /extensions/EasyCaptcha/template/jquery.events/jquery.event.drop.live-2.2.js	(revision 24215)
+++ /extensions/EasyCaptcha/template/jquery.events/jquery.event.drop.live-2.2.js	(revision 24215)
@@ -0,0 +1,93 @@
+/*! 
+ * jquery.event.drop.live - v 2.2
+ * Copyright (c) 2010 Three Dub Media - http://threedubmedia.com
+ * Open Source MIT License - http://threedubmedia.com/code/license
+ */
+// Created: 2010-06-07
+// Updated: 2012-05-21
+// REQUIRES: jquery 1.7.x, event.drag 2.2, event.drop 2.2
+
+;(function($){ // secure $ jQuery alias
+
+// local refs (increase compression)
+var $event = $.event,
+// ref the drop special event config
+drop = $event.special.drop,
+// old drop event add method
+origadd = drop.add,
+// old drop event teradown method
+origteardown = drop.teardown;
+
+// allow events to bubble for delegation
+drop.noBubble = false;
+
+// the namespace for internal live events
+drop.livekey = "livedrop";
+
+// new drop event add method
+drop.add = function( obj ){ 
+	// call the old method
+	origadd.apply( this, arguments );
+	// read the data
+	var data = $.data( this, drop.datakey );
+	// bind the live "dropinit" delegator
+	if ( !data.live && obj.selector ){
+		data.live = true;
+		$event.add( this, "dropinit."+ drop.livekey, drop.delegate );
+	}
+};
+
+// new drop event teardown method
+drop.teardown = function(){ 
+	// call the old method
+	origteardown.apply( this, arguments );
+	// read the data
+	var data = $.data( this, drop.datakey ) || {};
+	// remove the live "dropinit" delegator
+	if ( data.live ){
+		// remove the "live" delegation
+		$event.remove( this, "dropinit", drop.delegate );
+		data.live = false;
+	}
+};
+
+// identify potential delegate elements
+drop.delegate = function( event, dd ){
+	// local refs
+	var elems = [], $targets, 
+	// element event structure
+	events = $.data( this, "events" ) || {};
+	// query live events
+	$.each( events || [], function( key, arr ){
+		// no event type matches
+		if ( key.indexOf("drop") !== 0 )
+			return;
+		$.each( arr, function( i, obj ){
+			// locate the elements to delegate
+			$targets = $( event.currentTarget ).find( obj.selector );
+			// no element found
+			if ( !$targets.length ) 
+				return;
+			// take each target...
+			$targets.each(function(){
+				// add an event handler
+				$event.add( this, obj.origType +'.'+ drop.livekey, obj.origHandler || obj.handler, obj.data );
+				// remember new elements
+				if ( $.inArray( this, elems ) < 0 )
+					elems.push( this );	
+			});	
+		});
+	});
+	// may not exist when artifically triggering dropinit event
+	if ( dd )
+		// clean-up after the interaction ends
+		$event.add( dd.drag, "dragend."+drop.livekey, function(){
+			$.each( elems.concat( this ), function(){
+				$event.remove( this, '.'+ drop.livekey );							
+			});
+		});
+	//drop.delegates.push( elems );
+	return elems.length ? $( elems ) : false;
+};
+
+})( jQuery );
Index: /extensions/EasyCaptcha/template/register.tpl
===================================================================
--- /extensions/EasyCaptcha/template/register.tpl	(revision 24215)
+++ /extensions/EasyCaptcha/template/register.tpl	(revision 24215)
@@ -0,0 +1,14 @@
+</li>
+<li>
+  {include file=$EASYCAPTCHA_ABS_PATH|cat:'template/common.inc.tpl'}
+
+  {* <!-- DRAG & DROP --> *}
+  {if $EASYCAPTCHA_CHALLENGE == 'drag'}
+  <span class="property"><label>{'easycaptcha_drag_%s'|translate|sprintf:$EASYCAPTCHA_CONF.text}</label></span>
+
+  {* <!-- TIC TAC TOE --> *}
+  {else if $EASYCAPTCHA_CHALLENGE == 'tictac'}
+  <span class="property"><label>{'easycaptcha_tictac'|translate}</label></span>
+
+  {/if}
+  {$smarty.capture.easycaptcha}
Index: /extensions/EasyCaptcha/template/style.css
===================================================================
--- /extensions/EasyCaptcha/template/style.css	(revision 24215)
+++ /extensions/EasyCaptcha/template/style.css	(revision 24215)
@@ -0,0 +1,76 @@
+.theme {
+  display:inline-block;
+  position:relative;
+  width:66px;
+  height:66px;
+}
+  .theme .title {
+    display:block;
+    position:absolute;
+    top:-1em;
+    width:100%;
+    text-align:center;
+  }
+  .theme .count {
+    display:block;
+    position:absolute;
+    bottom:-1em;
+    width:100%;
+    text-align:center;
+    font-size:0.7em;
+  }
+  .theme img {
+    margin:1px;
+    padding:3px;
+    border:1px solid #999;
+    width:56px;
+    height:56px;
+  }
+  .theme.selected img {
+    border-color:#f70;
+  }
+
+.properties b {
+  display:inline-block;
+  min-width:200px;
+  text-align:right;
+  vertical-align:top;
+}
+
+.my-warnings {
+  background-image:none;
+  min-height:0;
+  padding:0 0 0 5px;
+  margin:0 5px 15px 5px;
+  font-weight:normal;
+  font-size:0.9em;
+}
+.my-warnings ul li {
+  list-style:none;
+}
+
+.chzn-choices li {
+  clear:none !important;
+}
+
+table.colors {
+  display:inline-block;
+  border-collapse:collapse;
+}
+  table.colors td, table.colors th {
+    padding:0 5px;
+    text-align:center;
+  }
+  table.colors td:nth-child(even), table.colors th:nth-child(even) {
+    background:rgba(127,127,127,0.2);
+  }
+
+
+.sp-palette-container {
+  max-width: 40px;
+}
+
+.preview {
+  display:none;
+  margin-left:200px;
+}
Index: /extensions/EasyCaptcha/tictac/functions_tictac.inc.php
===================================================================
--- /extensions/EasyCaptcha/tictac/functions_tictac.inc.php	(revision 24215)
+++ /extensions/EasyCaptcha/tictac/functions_tictac.inc.php	(revision 24215)
@@ -0,0 +1,272 @@
+<?php
+defined('EASYCAPTCHA_ID') or die('Hacking attempt!');
+
+function hex2rgb($hex)
+{
+  $hex = ltrim($hex, '#');
+
+  if (strlen($hex) == 3)
+  {
+    $hex = $hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2];
+  }
+  else if (strlen($hex) != 6)
+  {
+    return array(0,0,0);
+  }
+
+  $int = hexdec($hex);
+  return array(0xFF&($int>>0x10), 0xFF&($int>>0x8), 0xFF&$int);
+}
+
+function imagecolorallocatehex(&$img, $hex)
+{
+  $rgb = hex2rgb($hex);
+  return imagecolorallocate($img, $rgb[0], $rgb[1], $rgb[2]);
+}
+
+function imagegradientrectangle(&$img, $start, $end)
+{
+  $line_numbers = imagesx($img);
+  $line_width = imagesy($img);
+
+  list($r1,$g1,$b1) = $start;
+  list($r2,$g2,$b2) = $end;
+  list($r,$g,$b) = $end;
+
+  $fill = imagecolorallocate($img, $r, $g, $b);
+
+  for ($i=0; $i<$line_numbers; $i++)
+  {
+    $old_r = $r;
+    $old_g = $g;
+    $old_b = $b;
+
+    $r = ( $r2 - $r1 != 0 ) ? intval( $r1 + ( $r2 - $r1 ) * ( $i / $line_numbers ) ): $r1;
+    $g = ( $g2 - $g1 != 0 ) ? intval( $g1 + ( $g2 - $g1 ) * ( $i / $line_numbers ) ): $g1;
+    $b = ( $b2 - $b1 != 0 ) ? intval( $b1 + ( $b2 - $b1 ) * ( $i / $line_numbers ) ): $b1;
+
+    if ("$old_r $old_g $old_b" != "$r $g $b")
+    {
+      $fill = imagecolorallocate($img, $r, $g, $b);
+    }
+    imagefilledrectangle($img, 0, $i, $line_width, $i, $fill);
+  }
+}
+
+function drawcircle(&$img, $pos, $color)
+{
+  global $conf, $props, $img_circle;
+
+  if (!is_resource($img_circle))
+  {
+    $img_circle = imagecreatetruecolor($props['box_size'], $props['box_size']);
+
+    $bg = imagecolorallocatehex($img_circle, $conf['EasyCaptcha']['tictac']['bg1']);
+    $obj = imagecolorallocatehex($img_circle, $color);
+
+    imagefilledrectangle($img_circle, 0, 0, $props['box_size'], $props['box_size'], $bg);
+    imagecolortransparent($img_circle, $bg);
+
+    $radius = $props['box_size'] - $props['pad']*2;
+    $radius2 = $radius - 2*sqrt(2*$props['pad']*$props['pad']);
+
+    imagefilledellipse($img_circle, $props['box_size']/2, $props['box_size']/2, $radius, $radius, $obj);
+    imagefilledellipse($img_circle, $props['box_size']/2, $props['box_size']/2, $radius2, $radius2, $bg);
+  }
+
+  $pos = array(
+    $pos[0]*($props['bd_size']+$props['box_size']) + $props['bd_size'],
+    $pos[1]*($props['bd_size']+$props['box_size']) + $props['bd_size'],
+    );
+
+  imagecopymerge($img, $img_circle, $pos[0], $pos[1], 0, 0, $props['box_size'], $props['box_size'], 100);
+}
+
+function drawcross(&$img, $pos, $color)
+{
+  global $conf, $props, $img_cross;
+
+  if (!is_resource($img_cross))
+  {
+    $img_cross = imagecreatetruecolor($props['box_size'], $props['box_size']);
+
+    $bg = imagecolorallocatehex($img_cross, $conf['EasyCaptcha']['tictac']['bg1']);
+    $obj = imagecolorallocatehex($img_cross, $color);
+
+    imagefilledrectangle($img_cross, 0, 0, $props['box_size'], $props['box_size'], $bg);
+    imagecolortransparent($img_cross, $bg);
+
+    $points1 = array(
+      $props['pad']*2,             $props['pad'],
+      $props['box_size'] - $props['pad'],   $props['box_size'] - $props['pad']*2,
+      $props['box_size'] - $props['pad']*2, $props['box_size'] - $props['pad'],
+      $props['pad'],               $props['pad']*2,
+      );
+
+    $points2 = array(
+      $props['box_size'] - $props['pad']*2, $props['pad'],
+      $props['box_size'] - $props['pad'],   $props['pad']*2,
+      $props['pad']*2,             $props['box_size'] - $props['pad'],
+      $props['pad'],               $props['box_size'] - $props['pad']*2,
+      );
+
+    imagefilledpolygon($img_cross, $points1, 4, $obj);
+    imagefilledpolygon($img_cross, $points2, 4, $obj);
+  }
+
+  $pos = array(
+    $pos[0]*($props['bd_size']+$props['box_size']) + $props['bd_size'],
+    $pos[1]*($props['bd_size']+$props['box_size']) + $props['bd_size'],
+    );
+
+  imagecopymerge($img, $img_cross, $pos[0], $pos[1], 0, 0, $props['box_size'], $props['box_size'], 100);
+}
+
+function checkline($pos, $existing)
+{
+  $existing[$pos[0]][$pos[1]] = true;
+
+  // check col
+  $nb = 0;
+  for ($l=0; $l<3; $l++)
+  {
+    if (isset($existing[$pos[0]][$l])) $nb++;
+  }
+  if ($nb==3) return true;
+
+  // check line
+  $nb = 0;
+  for ($c=0; $c<3; $c++)
+  {
+    if (isset($existing[$c][$pos[1]])) $nb++;
+  }
+  if ($nb==3) return true;
+
+  // check diag 1
+  $nb = 0;
+  for ($i=0; $i<3; $i++)
+  {
+    if (isset($existing[$i][$i])) $nb++;
+  }
+  if ($nb==3) return true;
+
+  // check diag 2
+  $nb = 0;
+  for ($i=0; $i<3; $i++)
+  {
+    if (isset($existing[$i][2-$i])) $nb++;
+  }
+  if ($nb==3) return true;
+
+  return false;
+}
+
+function get_configs()
+{
+  return array(
+    // line 1
+    array(
+      'checked' => array(array(0,0),array(0,1)),
+      'answer' => array(0,2),
+      ),
+    array(
+      'checked' => array(array(0,0),array(0,2)),
+      'answer' => array(0,1),
+      ),
+    array(
+      'checked' => array(array(0,1),array(0,2)),
+      'answer' => array(0,0),
+      ),
+    // line 2
+    array(
+      'checked' => array(array(1,0),array(1,1)),
+      'answer' => array(1,2),
+      ),
+    array(
+      'checked' => array(array(1,0),array(1,2)),
+      'answer' => array(1,1),
+      ),
+    array(
+      'checked' => array(array(1,1),array(1,2)),
+      'answer' => array(1,0),
+      ),
+    // line 3
+    array(
+      'checked' => array(array(2,0),array(2,1)),
+      'answer' => array(2,2),
+      ),
+    array(
+      'checked' => array(array(2,0),array(2,2)),
+      'answer' => array(2,1),
+      ),
+    array(
+      'checked' => array(array(2,1),array(2,2)),
+      'answer' => array(2,0),
+      ),
+    // col 1
+    array(
+      'checked' => array(array(0,0),array(1,0)),
+      'answer' => array(2,0),
+      ),
+    array(
+      'checked' => array(array(0,0),array(2,0)),
+      'answer' => array(1,0),
+      ),
+    array(
+      'checked' => array(array(1,0),array(2,0)),
+      'answer' => array(0,0),
+      ),
+    // col 2
+    array(
+      'checked' => array(array(0,1),array(1,1)),
+      'answer' => array(2,1),
+      ),
+    array(
+      'checked' => array(array(0,1),array(2,1)),
+      'answer' => array(1,1),
+      ),
+    array(
+      'checked' => array(array(1,1),array(2,1)),
+      'answer' => array(0,1),
+      ),
+    // col 3
+    array(
+      'checked' => array(array(0,2),array(1,2)),
+      'answer' => array(2,2),
+      ),
+    array(
+      'checked' => array(array(0,2),array(2,2)),
+      'answer' => array(1,2),
+      ),
+    array(
+      'checked' => array(array(1,2),array(2,2)),
+      'answer' => array(0,2),
+      ),
+    // diag 1
+    array(
+      'checked' => array(array(0,0),array(1,1)),
+      'answer' => array(2,2),
+      ),
+    array(
+      'checked' => array(array(0,0),array(2,2)),
+      'answer' => array(1,1),
+      ),
+    array(
+      'checked' => array(array(1,1),array(2,2)),
+      'answer' => array(0,0),
+      ),
+    // diag 2
+    array(
+      'checked' => array(array(2,0),array(1,1)),
+      'answer' => array(0,2),
+      ),
+    array(
+      'checked' => array(array(2,0),array(0,2)),
+      'answer' => array(1,1),
+      ),
+    array(
+      'checked' => array(array(1,1),array(0,2)),
+      'answer' => array(2,0),
+      ),
+    );
+}
Index: /extensions/EasyCaptcha/tictac/gen.php
===================================================================
--- /extensions/EasyCaptcha/tictac/gen.php	(revision 24215)
+++ /extensions/EasyCaptcha/tictac/gen.php	(revision 24215)
@@ -0,0 +1,100 @@
+<?php
+define('PHPWG_ROOT_PATH', '../../../');
+include(PHPWG_ROOT_PATH . 'include/common.inc.php');
+
+defined('EASYCAPTCHA_ID') or die('Hacking attempt!');
+include_once(EASYCAPTCHA_PATH . 'tictac/functions_tictac.inc.php');
+
+$props = array();
+
+// special size when asking for cross only
+$props['size'] = isset($_GET['cross']) ? intval($_GET['cross']) : $conf['EasyCaptcha']['tictac']['size'];
+
+// compute various sizes
+$props['bd_size'] = max(1, floor($props['size']*0.01));
+$props['box_size'] = floor(($props['size']-4*$props['bd_size'])/3);
+$props['size'] = 3*$props['box_size'] + 4*$props['bd_size'] + 1;
+$props['pad'] = floor($props['box_size']*0.1);
+$props['radius'] = floor($props['box_size']*0.2);
+
+
+// return cross only
+if (isset($_GET['cross']))
+{
+  $props['bd_size']/= 2;
+
+  $img = imagecreatetruecolor($props['box_size'], $props['box_size']);
+  imageantialias($img, true);
+
+  $bg = imagecolorallocatehex($img, $conf['EasyCaptcha']['tictac']['bg1']);
+
+  imagefilledrectangle($img, 0, 0, $props['box_size'], $props['box_size'], $bg);
+  imagecolortransparent($img, $bg);
+
+  drawcross($img, array(0,0), $conf['EasyCaptcha']['tictac']['sel']);
+
+  header('Content-Type: image/png');
+  imagepng($img);
+  exit;
+}
+
+
+// pick a random config
+$configs = get_configs();
+$selection = $configs[ array_rand($configs) ];
+pwg_set_session_var('easycaptcha', implode('', $selection['answer']));
+
+
+// create image
+$img = imagecreatetruecolor($props['size'], $props['size']);
+imageantialias($img, true);
+
+// background
+$bg_start = hex2rgb($conf['EasyCaptcha']['tictac']['bg1']);
+$bg_end = hex2rgb($conf['EasyCaptcha']['tictac']['bg2']);
+
+imagegradientrectangle($img, $bg_start , $bg_end);
+// $bg = imagecolorallocatehex($img, $conf['EasyCaptcha']['tictac']['bg1']);
+// imagefilledrectangle($img, 0, 0, $props['size'], $props['size'], $bg);
+
+// borders
+$bd = imagecolorallocatehex($img, $conf['EasyCaptcha']['tictac']['bd']);
+for ($i=0; $i<4; $i++)
+{
+  imagefilledrectangle($img, $i*($props['box_size']+$props['bd_size']), 0, $i*($props['box_size']+$props['bd_size'])+$props['bd_size'], $props['size'], $bd);
+  imagefilledrectangle($img, 0, $i*($props['box_size']+$props['bd_size']), $props['size'], $i*($props['box_size']+$props['bd_size'])+$props['bd_size'], $bd);
+}
+
+// crosses
+foreach ($selection['checked'] as $pos)
+{
+  drawcross($img, $pos, $conf['EasyCaptcha']['tictac']['obj']);
+}
+
+// circles
+$protect = $selection['checked'];
+$protect[] = $selection['answer'];
+$i = rand(2,4);
+$circles = array();
+
+while ($i>0)
+{
+  $pos = array(rand(0, 2), rand(0, 2));
+
+  foreach ($protect as $pro)
+  {
+    if ($pos[0]==$pro[0] && $pos[1]==$pro[1]) continue(2);
+  }
+  if (checkline($pos, $circles)) continue;
+
+  $protect[] = $pos;
+  $circles[$pos[0]][$pos[1]] = true;
+
+  drawcircle($img, $pos, $conf['EasyCaptcha']['tictac']['obj']);
+  $i--;
+}
+
+
+// output
+header('Content-Type: image/png');
+imagepng($img);
Index: /extensions/EasyCaptcha/tictac/gen_admin.php
===================================================================
--- /extensions/EasyCaptcha/tictac/gen_admin.php	(revision 24215)
+++ /extensions/EasyCaptcha/tictac/gen_admin.php	(revision 24215)
@@ -0,0 +1,83 @@
+<?php
+define('PHPWG_ROOT_PATH', '../../../');
+include(PHPWG_ROOT_PATH . 'include/common.inc.php');
+
+is_admin() or die('Hacking attempt!');
+
+defined('EASYCAPTCHA_ID') or die('Hacking attempt!');
+include_once(EASYCAPTCHA_PATH . 'tictac/functions_tictac.inc.php');
+
+$props = array();
+
+// overwrite config from url params
+$conf['EasyCaptcha']['tictac'] = $_GET;
+$props['size'] = $conf['EasyCaptcha']['tictac']['size'];
+
+// compute various sizes
+$props['bd_size'] = max(1, floor($props['size']*0.01));
+$props['box_size'] = floor(($props['size']-4*$props['bd_size'])/3);
+$props['size'] = 3*$props['box_size'] + 4*$props['bd_size'] + 1;
+$props['pad'] = floor($props['box_size']*0.1);
+$props['radius'] = floor($props['box_size']*0.2);
+
+
+// pick a random config
+$configs = get_configs();
+$selection = $configs[ array_rand($configs) ];
+
+
+// create image
+$img = imagecreatetruecolor($props['size'], $props['size']);
+imageantialias($img, true);
+
+// background
+$bg_start = hex2rgb($conf['EasyCaptcha']['tictac']['bg1']);
+$bg_end = hex2rgb($conf['EasyCaptcha']['tictac']['bg2']);
+
+imagegradientrectangle($img, $bg_start , $bg_end);
+// $bg = imagecolorallocatehex($img, $conf['EasyCaptcha']['tictac']['bg1']);
+// imagefilledrectangle($img, 0, 0, $props['size'], $props['size'], $bg);
+
+// borders
+$bd = imagecolorallocatehex($img, $conf['EasyCaptcha']['tictac']['bd']);
+for ($i=0; $i<4; $i++)
+{
+  imagefilledrectangle($img, $i*($props['box_size']+$props['bd_size']), 0, $i*($props['box_size']+$props['bd_size'])+$props['bd_size'], $props['size'], $bd);
+  imagefilledrectangle($img, 0, $i*($props['box_size']+$props['bd_size']), $props['size'], $i*($props['box_size']+$props['bd_size'])+$props['bd_size'], $bd);
+}
+
+// crosses
+foreach ($selection['checked'] as $pos)
+{
+  drawcross($img, $pos, $conf['EasyCaptcha']['tictac']['obj']);
+}
+imagedestroy($img_cross);
+drawcross($img, $selection['answer'], $conf['EasyCaptcha']['tictac']['sel']);
+
+// circles
+$protect = $selection['checked'];
+$protect[] = $selection['answer'];
+$i = rand(2,4);
+$circles = array();
+
+while ($i>0)
+{
+  $pos = array(rand(0, 2), rand(0, 2));
+
+  foreach ($protect as $pro)
+  {
+    if ($pos[0]==$pro[0] && $pos[1]==$pro[1]) continue(2);
+  }
+  if (checkline($pos, $circles)) continue;
+
+  $protect[] = $pos;
+  $circles[$pos[0]][$pos[1]] = true;
+
+  drawcircle($img, $pos, $conf['EasyCaptcha']['tictac']['obj']);
+  $i--;
+}
+
+
+// output
+header('Content-Type: image/png');
+imagepng($img);
Index: /extensions/EasyCaptcha/tictac/index.php
===================================================================
--- /extensions/EasyCaptcha/tictac/index.php	(revision 24215)
+++ /extensions/EasyCaptcha/tictac/index.php	(revision 24215)
@@ -0,0 +1,7 @@
+<?php
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
