diff --git a/README.md b/README.md
index 63eae4967a6558339879c92e98743dd950d7f56b..4f820dba8f8cef3a1e01289b326d334b03753788 100644
--- a/README.md
+++ b/README.md
@@ -86,3 +86,4 @@ osTicket is supported by several magical open source projects including:
   * [PEAR/Net_Socket](http://pear.php.net/package/Net_Socket)
   * [PEAR/Serivces_JSON](http://pear.php.net/package/Services_JSON)
   * [phplint](http://antirez.com/page/phplint.html)
+  * [Spyc](http://github.com/mustangostang/spyc)
diff --git a/include/Spyc.php b/include/Spyc.php
new file mode 100644
index 0000000000000000000000000000000000000000..17d67db3ea32baa729606efc7e42b6468b0d8131
--- /dev/null
+++ b/include/Spyc.php
@@ -0,0 +1,1084 @@
+<?php
+/**
+   * Spyc -- A Simple PHP YAML Class
+   * @version 0.5.1
+   * @author Vlad Andersen <vlad.andersen@gmail.com>
+   * @author Chris Wanstrath <chris@ozmm.org>
+   * @link http://code.google.com/p/spyc/
+   * @copyright Copyright 2005-2006 Chris Wanstrath, 2006-2011 Vlad Andersen
+   * @license http://www.opensource.org/licenses/mit-license.php MIT License
+   * @package Spyc
+   */
+
+if (!function_exists('spyc_load')) {
+  /**
+   * Parses YAML to array.
+   * @param string $string YAML string.
+   * @return array
+   */
+  function spyc_load ($string) {
+    return Spyc::YAMLLoadString($string);
+  }
+}
+
+if (!function_exists('spyc_load_file')) {
+  /**
+   * Parses YAML to array.
+   * @param string $file Path to YAML file.
+   * @return array
+   */
+  function spyc_load_file ($file) {
+    return Spyc::YAMLLoad($file);
+  }
+}
+
+if (!function_exists('spyc_dump')) {
+  /**
+   * Dumps array to YAML.
+   * @param array $data Array.
+   * @return string
+   */
+  function spyc_dump ($data) {
+    return Spyc::YAMLDump($data, false, false, true);
+  }
+}
+
+/**
+   * The Simple PHP YAML Class.
+   *
+   * This class can be used to read a YAML file and convert its contents
+   * into a PHP array.  It currently supports a very limited subsection of
+   * the YAML spec.
+   *
+   * Usage:
+   * <code>
+   *   $Spyc  = new Spyc;
+   *   $array = $Spyc->load($file);
+   * </code>
+   * or:
+   * <code>
+   *   $array = Spyc::YAMLLoad($file);
+   * </code>
+   * or:
+   * <code>
+   *   $array = spyc_load_file($file);
+   * </code>
+   * @package Spyc
+   */
+class Spyc {
+
+  // SETTINGS
+
+  const REMPTY = "\0\0\0\0\0";
+
+  /**
+   * Setting this to true will force YAMLDump to enclose any string value in
+   * quotes.  False by default.
+   *
+   * @var bool
+   */
+  var $setting_dump_force_quotes = false;
+
+  /**
+   * Setting this to true will forse YAMLLoad to use syck_load function when
+   * possible. False by default.
+   * @var bool
+   */
+  var $setting_use_syck_is_possible = false;
+
+
+
+  /**#@+
+  * @access private
+  * @var mixed
+  */
+  var $_dumpIndent;
+  var $_dumpWordWrap;
+  var $_containsGroupAnchor = false;
+  var $_containsGroupAlias = false;
+  var $path;
+  var $result;
+  var $LiteralPlaceHolder = '___YAML_Literal_Block___';
+  var $SavedGroups = array();
+  var $indent;
+  /**
+   * Path modifier that should be applied after adding current element.
+   * @var array
+   */
+  var $delayedPath = array();
+
+  /**#@+
+  * @access public
+  * @var mixed
+  */
+  var $_nodeId;
+
+/**
+ * Load a valid YAML string to Spyc.
+ * @param string $input
+ * @return array
+ */
+  function load ($input) {
+    return $this->__loadString($input);
+  }
+
+ /**
+ * Load a valid YAML file to Spyc.
+ * @param string $file
+ * @return array
+ */
+  function loadFile ($file) {
+    return $this->__load($file);
+  }
+
+  /**
+     * Load YAML into a PHP array statically
+     *
+     * The load method, when supplied with a YAML stream (string or file),
+     * will do its best to convert YAML in a file into a PHP array.  Pretty
+     * simple.
+     *  Usage:
+     *  <code>
+     *   $array = Spyc::YAMLLoad('lucky.yaml');
+     *   print_r($array);
+     *  </code>
+     * @access public
+     * @return array
+     * @param string $input Path of YAML file or string containing YAML
+     */
+  function YAMLLoad($input) {
+    $Spyc = new Spyc;
+    return $Spyc->__load($input);
+  }
+
+  /**
+     * Load a string of YAML into a PHP array statically
+     *
+     * The load method, when supplied with a YAML string, will do its best
+     * to convert YAML in a string into a PHP array.  Pretty simple.
+     *
+     * Note: use this function if you don't want files from the file system
+     * loaded and processed as YAML.  This is of interest to people concerned
+     * about security whose input is from a string.
+     *
+     *  Usage:
+     *  <code>
+     *   $array = Spyc::YAMLLoadString("---\n0: hello world\n");
+     *   print_r($array);
+     *  </code>
+     * @access public
+     * @return array
+     * @param string $input String containing YAML
+     */
+  function YAMLLoadString($input) {
+    $Spyc = new Spyc;
+    return $Spyc->__loadString($input);
+  }
+
+  /**
+     * Dump YAML from PHP array statically
+     *
+     * The dump method, when supplied with an array, will do its best
+     * to convert the array into friendly YAML.  Pretty simple.  Feel free to
+     * save the returned string as nothing.yaml and pass it around.
+     *
+     * Oh, and you can decide how big the indent is and what the wordwrap
+     * for folding is.  Pretty cool -- just pass in 'false' for either if
+     * you want to use the default.
+     *
+     * Indent's default is 2 spaces, wordwrap's default is 40 characters.  And
+     * you can turn off wordwrap by passing in 0.
+     *
+     * @access public
+     * @return string
+     * @param array $array PHP array
+     * @param int $indent Pass in false to use the default, which is 2
+     * @param int $wordwrap Pass in 0 for no wordwrap, false for default (40)
+     * @param int $no_opening_dashes Do not start YAML file with "---\n"
+     */
+  function YAMLDump($array, $indent = false, $wordwrap = false, $no_opening_dashes = false) {
+    $spyc = new Spyc;
+    return $spyc->dump($array, $indent, $wordwrap, $no_opening_dashes);
+  }
+
+
+  /**
+     * Dump PHP array to YAML
+     *
+     * The dump method, when supplied with an array, will do its best
+     * to convert the array into friendly YAML.  Pretty simple.  Feel free to
+     * save the returned string as tasteful.yaml and pass it around.
+     *
+     * Oh, and you can decide how big the indent is and what the wordwrap
+     * for folding is.  Pretty cool -- just pass in 'false' for either if
+     * you want to use the default.
+     *
+     * Indent's default is 2 spaces, wordwrap's default is 40 characters.  And
+     * you can turn off wordwrap by passing in 0.
+     *
+     * @access public
+     * @return string
+     * @param array $array PHP array
+     * @param int $indent Pass in false to use the default, which is 2
+     * @param int $wordwrap Pass in 0 for no wordwrap, false for default (40)
+     */
+  function dump($array,$indent = false,$wordwrap = false, $no_opening_dashes = false) {
+    // Dumps to some very clean YAML.  We'll have to add some more features
+    // and options soon.  And better support for folding.
+
+    // New features and options.
+    if ($indent === false or !is_numeric($indent)) {
+      $this->_dumpIndent = 2;
+    } else {
+      $this->_dumpIndent = $indent;
+    }
+
+    if ($wordwrap === false or !is_numeric($wordwrap)) {
+      $this->_dumpWordWrap = 40;
+    } else {
+      $this->_dumpWordWrap = $wordwrap;
+    }
+
+    // New YAML document
+    $string = "";
+    if (!$no_opening_dashes) $string = "---\n";
+
+    // Start at the base of the array and move through it.
+    if ($array) {
+      $array = (array)$array;
+      $previous_key = -1;
+      foreach ($array as $key => $value) {
+        if (!isset($first_key)) $first_key = $key;
+        $string .= $this->_yamlize($key,$value,0,$previous_key, $first_key, $array);
+        $previous_key = $key;
+      }
+    }
+    return $string;
+  }
+
+  /**
+     * Attempts to convert a key / value array item to YAML
+     * @access private
+     * @return string
+     * @param $key The name of the key
+     * @param $value The value of the item
+     * @param $indent The indent of the current node
+     */
+  function _yamlize($key,$value,$indent, $previous_key = -1, $first_key = 0, $source_array = null) {
+    if (is_array($value)) {
+      if (empty ($value))
+        return $this->_dumpNode($key, array(), $indent, $previous_key, $first_key, $source_array);
+      // It has children.  What to do?
+      // Make it the right kind of item
+      $string = $this->_dumpNode($key, $this->REMPTY, $indent, $previous_key, $first_key, $source_array);
+      // Add the indent
+      $indent += $this->_dumpIndent;
+      // Yamlize the array
+      $string .= $this->_yamlizeArray($value,$indent);
+    } elseif (!is_array($value)) {
+      // It doesn't have children.  Yip.
+      $string = $this->_dumpNode($key, $value, $indent, $previous_key, $first_key, $source_array);
+    }
+    return $string;
+  }
+
+  /**
+     * Attempts to convert an array to YAML
+     * @access private
+     * @return string
+     * @param $array The array you want to convert
+     * @param $indent The indent of the current level
+     */
+  function _yamlizeArray($array,$indent) {
+    if (is_array($array)) {
+      $string = '';
+      $previous_key = -1;
+      foreach ($array as $key => $value) {
+        if (!isset($first_key)) $first_key = $key;
+        $string .= $this->_yamlize($key, $value, $indent, $previous_key, $first_key, $array);
+        $previous_key = $key;
+      }
+      return $string;
+    } else {
+      return false;
+    }
+  }
+
+  /**
+     * Returns YAML from a key and a value
+     * @access private
+     * @return string
+     * @param $key The name of the key
+     * @param $value The value of the item
+     * @param $indent The indent of the current node
+     */
+  function _dumpNode($key, $value, $indent, $previous_key = -1, $first_key = 0, $source_array = null) {
+    // do some folding here, for blocks
+    if (is_string ($value) && ((strpos($value,"\n") !== false || strpos($value,": ") !== false || strpos($value,"- ") !== false ||
+      strpos($value,"*") !== false || strpos($value,"#") !== false || strpos($value,"<") !== false || strpos($value,">") !== false || strpos ($value, '  ') !== false ||
+      strpos($value,"[") !== false || strpos($value,"]") !== false || strpos($value,"{") !== false || strpos($value,"}") !== false) || strpos($value,"&") !== false || strpos($value, "'") !== false || strpos($value, "!") === 0 ||
+      substr ($value, -1, 1) == ':')
+    ) {
+      $value = $this->_doLiteralBlock($value,$indent);
+    } else {
+      $value  = $this->_doFolding($value,$indent);
+    }
+
+    if ($value === array()) $value = '[ ]';
+    if ($value === "") $value = '""';
+    if (in_array ($value, array ('true', 'TRUE', 'false', 'FALSE', 'y', 'Y', 'n', 'N', 'null', 'NULL'), true)) {
+       $value = $this->_doLiteralBlock($value,$indent);
+    }
+    if (trim ($value) != $value)
+       $value = $this->_doLiteralBlock($value,$indent);
+
+    if (is_bool($value)) {
+       $value = ($value) ? "true" : "false";
+    }
+
+    if ($value === null) $value = 'null';
+    if ($value === "'" . $this->REMPTY . "'") $value = null;
+
+    $spaces = str_repeat(' ',$indent);
+
+    //if (is_int($key) && $key - 1 == $previous_key && $first_key===0) {
+    if (is_array ($source_array) && array_keys($source_array) === range(0, count($source_array) - 1)) {
+      // It's a sequence
+      $string = $spaces.'- '.$value."\n";
+    } else {
+      // if ($first_key===0)  trigger_error('Keys are all screwy.  The first one was zero, now it\'s "'. $key .'"',E_USER_ERROR);
+      // It's mapped
+      if (strpos($key, ":") !== false || strpos($key, "#") !== false) { $key = '"' . $key . '"'; }
+      $string = rtrim ($spaces.$key.': '.$value)."\n";
+    }
+    return $string;
+  }
+
+  /**
+     * Creates a literal block for dumping
+     * @access private
+     * @return string
+     * @param $value
+     * @param $indent int The value of the indent
+     */
+  function _doLiteralBlock($value,$indent) {
+    if ($value === "\n") return '\n';
+    if (strpos($value, "\n") === false && strpos($value, "'") === false) {
+      return sprintf ("'%s'", $value);
+    }
+    if (strpos($value, "\n") === false && strpos($value, '"') === false) {
+      return sprintf ('"%s"', $value);
+    }
+    $exploded = explode("\n",$value);
+    $newValue = '|';
+    $indent  += $this->_dumpIndent;
+    $spaces   = str_repeat(' ',$indent);
+    foreach ($exploded as $line) {
+      $newValue .= "\n" . $spaces . ($line);
+    }
+    return $newValue;
+  }
+
+  /**
+     * Folds a string of text, if necessary
+     * @access private
+     * @return string
+     * @param $value The string you wish to fold
+     */
+  function _doFolding($value,$indent) {
+    // Don't do anything if wordwrap is set to 0
+
+    if ($this->_dumpWordWrap !== 0 && is_string ($value) && strlen($value) > $this->_dumpWordWrap) {
+      $indent += $this->_dumpIndent;
+      $indent = str_repeat(' ',$indent);
+      $wrapped = wordwrap($value,$this->_dumpWordWrap,"\n$indent");
+      $value   = ">\n".$indent.$wrapped;
+    } else {
+      if ($this->setting_dump_force_quotes && is_string ($value) && $value !== $this->REMPTY)
+        $value = '"' . $value . '"';
+      if (is_numeric($value) && is_string($value))
+        $value = '"' . $value . '"';
+    }
+
+
+    return $value;
+  }
+
+// LOADING FUNCTIONS
+
+  function __load($input) {
+    $Source = $this->loadFromSource($input);
+    return $this->loadWithSource($Source);
+  }
+
+  function __loadString($input) {
+    $Source = $this->loadFromString($input);
+    return $this->loadWithSource($Source);
+  }
+
+  function loadWithSource($Source) {
+    if (empty ($Source)) return array();
+    if ($this->setting_use_syck_is_possible && function_exists ('syck_load')) {
+      $array = syck_load (implode ("\n", $Source));
+      return is_array($array) ? $array : array();
+    }
+
+    $this->path = array();
+    $this->result = array();
+
+    $cnt = count($Source);
+    for ($i = 0; $i < $cnt; $i++) {
+      $line = $Source[$i];
+
+      $this->indent = strlen($line) - strlen(ltrim($line));
+      $tempPath = $this->getParentPathByIndent($this->indent);
+      $line = $this->stripIndent($line, $this->indent);
+      if ($this->isComment($line)) continue;
+      if ($this->isEmpty($line)) continue;
+      $this->path = $tempPath;
+
+      $literalBlockStyle = $this->startsLiteralBlock($line);
+      if ($literalBlockStyle) {
+        $line = rtrim ($line, $literalBlockStyle . " \n");
+        $literalBlock = '';
+        $line .= $this->LiteralPlaceHolder;
+        $literal_block_indent = strlen($Source[$i+1]) - strlen(ltrim($Source[$i+1]));
+        while (++$i < $cnt && $this->literalBlockContinues($Source[$i], $this->indent)) {
+          $literalBlock = $this->addLiteralLine($literalBlock, $Source[$i], $literalBlockStyle, $literal_block_indent);
+        }
+        $i--;
+      }
+
+      // Strip out comments
+      if (strpos ($line, '#')) {
+          $line = preg_replace('/\s*#([^"\']+)$/','',$line);
+      }
+
+      while (++$i < $cnt && $this->greedilyNeedNextLine($line)) {
+        $line = rtrim ($line, " \n\t\r") . ' ' . ltrim ($Source[$i], " \t");
+      }
+      $i--;
+
+      $lineArray = $this->_parseLine($line);
+
+      if ($literalBlockStyle)
+        $lineArray = $this->revertLiteralPlaceHolder ($lineArray, $literalBlock);
+
+      $this->addArray($lineArray, $this->indent);
+
+      foreach ($this->delayedPath as $indent => $delayedPath)
+        $this->path[$indent] = $delayedPath;
+
+      $this->delayedPath = array();
+
+    }
+    return $this->result;
+  }
+
+  function loadFromSource ($input) {
+    if (!empty($input) && strpos($input, "\n") === false && file_exists($input))
+      $input = file_get_contents($input);
+
+    return $this->loadFromString($input);
+  }
+
+  function loadFromString ($input) {
+    $lines = explode("\n",$input);
+    foreach ($lines as $k => $_) {
+      $lines[$k] = rtrim ($_, "\r");
+    }
+    return $lines;
+  }
+
+  /**
+     * Parses YAML code and returns an array for a node
+     * @access private
+     * @return array
+     * @param string $line A line from the YAML file
+     */
+  function _parseLine($line) {
+    if (!$line) return array();
+    $line = trim($line);
+    if (!$line) return array();
+
+    $array = array();
+
+    $group = $this->nodeContainsGroup($line);
+    if ($group) {
+      $this->addGroup($line, $group);
+      $line = $this->stripGroup ($line, $group);
+    }
+
+    if ($this->startsMappedSequence($line))
+      return $this->returnMappedSequence($line);
+
+    if ($this->startsMappedValue($line))
+      return $this->returnMappedValue($line);
+
+    if ($this->isArrayElement($line))
+     return $this->returnArrayElement($line);
+
+    if ($this->isPlainArray($line))
+     return $this->returnPlainArray($line);
+
+
+    return $this->returnKeyValuePair($line);
+
+  }
+
+  /**
+     * Finds the type of the passed value, returns the value as the new type.
+     * @access private
+     * @param string $value
+     * @return mixed
+     */
+  function _toType($value) {
+    if ($value === '') return "";
+    $first_character = $value[0];
+    $last_character = substr($value, -1, 1);
+
+    $is_quoted = false;
+    do {
+      if (!$value) break;
+      if ($first_character != '"' && $first_character != "'") break;
+      if ($last_character != '"' && $last_character != "'") break;
+      $is_quoted = true;
+    } while (0);
+
+    if ($is_quoted)
+      return strtr(substr ($value, 1, -1), array ('\\"' => '"', '\'\'' => '\'', '\\\'' => '\''));
+
+    if (strpos($value, ' #') !== false && !$is_quoted)
+      $value = preg_replace('/\s+#(.+)$/','',$value);
+
+    if (!$is_quoted) $value = str_replace('\n', "\n", $value);
+
+    if ($first_character == '[' && $last_character == ']') {
+      // Take out strings sequences and mappings
+      $innerValue = trim(substr ($value, 1, -1));
+      if ($innerValue === '') return array();
+      $explode = $this->_inlineEscape($innerValue);
+      // Propagate value array
+      $value  = array();
+      foreach ($explode as $v) {
+        $value[] = $this->_toType($v);
+      }
+      return $value;
+    }
+
+    if (strpos($value,': ')!==false && $first_character != '{') {
+      $array = explode(': ',$value);
+      $key   = trim($array[0]);
+      array_shift($array);
+      $value = trim(implode(': ',$array));
+      $value = $this->_toType($value);
+      return array($key => $value);
+    }
+
+    if ($first_character == '{' && $last_character == '}') {
+      $innerValue = trim(substr ($value, 1, -1));
+      if ($innerValue === '') return array();
+      // Inline Mapping
+      // Take out strings sequences and mappings
+      $explode = $this->_inlineEscape($innerValue);
+      // Propagate value array
+      $array = array();
+      foreach ($explode as $v) {
+        $SubArr = $this->_toType($v);
+        if (empty($SubArr)) continue;
+        if (is_array ($SubArr)) {
+          $array[key($SubArr)] = $SubArr[key($SubArr)]; continue;
+        }
+        $array[] = $SubArr;
+      }
+      return $array;
+    }
+
+    if ($value == 'null' || $value == 'NULL' || $value == 'Null' || $value == '' || $value == '~') {
+      return null;
+    }
+
+    if ( is_numeric($value) && preg_match ('/^(-|)[1-9]+[0-9]*$/', $value) ){
+      $intvalue = (int)$value;
+      if ($intvalue != PHP_INT_MAX)
+        $value = $intvalue;
+      return $value;
+    }
+
+    if (in_array($value,
+                 array('true', 'on', '+', 'yes', 'y', 'True', 'TRUE', 'On', 'ON', 'YES', 'Yes', 'Y'))) {
+      return true;
+    }
+
+    if (in_array(strtolower($value),
+                 array('false', 'off', '-', 'no', 'n'))) {
+      return false;
+    }
+
+    if (is_numeric($value)) {
+      if ($value === '0') return 0;
+      if (rtrim ($value, 0) === $value)
+        $value = (float)$value;
+      return $value;
+    }
+
+    return $value;
+  }
+
+  /**
+     * Used in inlines to check for more inlines or quoted strings
+     * @access private
+     * @return array
+     */
+  function _inlineEscape($inline) {
+    // There's gotta be a cleaner way to do this...
+    // While pure sequences seem to be nesting just fine,
+    // pure mappings and mappings with sequences inside can't go very
+    // deep.  This needs to be fixed.
+
+    $seqs = array();
+    $maps = array();
+    $saved_strings = array();
+    $saved_empties = array();
+
+    // Check for empty strings
+    $regex = '/("")|(\'\')/';
+    if (preg_match_all($regex,$inline,$strings)) {
+      $saved_empties = $strings[0];
+      $inline  = preg_replace($regex,'YAMLEmpty',$inline);
+    }
+    unset($regex);
+
+    // Check for strings
+    $regex = '/(?:(")|(?:\'))((?(1)[^"]+|[^\']+))(?(1)"|\')/';
+    if (preg_match_all($regex,$inline,$strings)) {
+      $saved_strings = $strings[0];
+      $inline  = preg_replace($regex,'YAMLString',$inline);
+    }
+    unset($regex);
+
+    // echo $inline;
+
+    $i = 0;
+    do {
+
+    // Check for sequences
+    while (preg_match('/\[([^{}\[\]]+)\]/U',$inline,$matchseqs)) {
+      $seqs[] = $matchseqs[0];
+      $inline = preg_replace('/\[([^{}\[\]]+)\]/U', ('YAMLSeq' . (count($seqs) - 1) . 's'), $inline, 1);
+    }
+
+    // Check for mappings
+    while (preg_match('/{([^\[\]{}]+)}/U',$inline,$matchmaps)) {
+      $maps[] = $matchmaps[0];
+      $inline = preg_replace('/{([^\[\]{}]+)}/U', ('YAMLMap' . (count($maps) - 1) . 's'), $inline, 1);
+    }
+
+    if ($i++ >= 10) break;
+
+    } while (strpos ($inline, '[') !== false || strpos ($inline, '{') !== false);
+
+    $explode = explode(',',$inline);
+    $explode = array_map('trim', $explode);
+    $stringi = 0; $i = 0;
+
+    while (1) {
+
+    // Re-add the sequences
+    if (!empty($seqs)) {
+      foreach ($explode as $key => $value) {
+        if (strpos($value,'YAMLSeq') !== false) {
+          foreach ($seqs as $seqk => $seq) {
+            $explode[$key] = str_replace(('YAMLSeq'.$seqk.'s'),$seq,$value);
+            $value = $explode[$key];
+          }
+        }
+      }
+    }
+
+    // Re-add the mappings
+    if (!empty($maps)) {
+      foreach ($explode as $key => $value) {
+        if (strpos($value,'YAMLMap') !== false) {
+          foreach ($maps as $mapk => $map) {
+            $explode[$key] = str_replace(('YAMLMap'.$mapk.'s'), $map, $value);
+            $value = $explode[$key];
+          }
+        }
+      }
+    }
+
+
+    // Re-add the strings
+    if (!empty($saved_strings)) {
+      foreach ($explode as $key => $value) {
+        while (strpos($value,'YAMLString') !== false) {
+          $explode[$key] = preg_replace('/YAMLString/',$saved_strings[$stringi],$value, 1);
+          unset($saved_strings[$stringi]);
+          ++$stringi;
+          $value = $explode[$key];
+        }
+      }
+    }
+
+
+    // Re-add the empties
+    if (!empty($saved_empties)) {
+      foreach ($explode as $key => $value) {
+        while (strpos($value,'YAMLEmpty') !== false) {
+          $explode[$key] = preg_replace('/YAMLEmpty/', '', $value, 1);
+          $value = $explode[$key];
+        }
+      }
+    }
+
+    $finished = true;
+    foreach ($explode as $key => $value) {
+      if (strpos($value,'YAMLSeq') !== false) {
+        $finished = false; break;
+      }
+      if (strpos($value,'YAMLMap') !== false) {
+        $finished = false; break;
+      }
+      if (strpos($value,'YAMLString') !== false) {
+        $finished = false; break;
+      }
+      if (strpos($value,'YAMLEmpty') !== false) {
+        $finished = false; break;
+      }
+    }
+    if ($finished) break;
+
+    $i++;
+    if ($i > 10)
+      break; // Prevent infinite loops.
+    }
+
+
+    return $explode;
+  }
+
+  function literalBlockContinues ($line, $lineIndent) {
+    if (!trim($line)) return true;
+    if (strlen($line) - strlen(ltrim($line)) > $lineIndent) return true;
+    return false;
+  }
+
+  function referenceContentsByAlias ($alias) {
+    do {
+      if (!isset($this->SavedGroups[$alias])) { echo "Bad group name: $alias."; break; }
+      $groupPath = $this->SavedGroups[$alias];
+      $value = $this->result;
+      foreach ($groupPath as $k) {
+        $value = $value[$k];
+      }
+    } while (false);
+    return $value;
+  }
+
+  function addArrayInline ($array, $indent) {
+      $CommonGroupPath = $this->path;
+      if (empty ($array)) return false;
+
+      foreach ($array as $k => $_) {
+        $this->addArray(array($k => $_), $indent);
+        $this->path = $CommonGroupPath;
+      }
+      return true;
+  }
+
+  function addArray ($incoming_data, $incoming_indent) {
+
+   // print_r ($incoming_data);
+
+    if (count ($incoming_data) > 1)
+      return $this->addArrayInline ($incoming_data, $incoming_indent);
+
+    $key = key ($incoming_data);
+    $value = isset($incoming_data[$key]) ? $incoming_data[$key] : null;
+    if ($key === '__!YAMLZero') $key = '0';
+
+    if ($incoming_indent == 0 && !$this->_containsGroupAlias && !$this->_containsGroupAnchor) { // Shortcut for root-level values.
+      if ($key || $key === '' || $key === '0') {
+        $this->result[$key] = $value;
+      } else {
+        $this->result[] = $value; end ($this->result); $key = key ($this->result);
+      }
+      $this->path[$incoming_indent] = $key;
+      return;
+    }
+
+
+
+    $history = array();
+    // Unfolding inner array tree.
+    $history[] = $_arr = $this->result;
+    foreach ($this->path as $k) {
+      $history[] = $_arr = $_arr[$k];
+    }
+
+    if ($this->_containsGroupAlias) {
+      $value = $this->referenceContentsByAlias($this->_containsGroupAlias);
+      $this->_containsGroupAlias = false;
+    }
+
+
+    // Adding string or numeric key to the innermost level or $this->arr.
+    if (is_string($key) && $key == '<<') {
+      if (!is_array ($_arr)) { $_arr = array (); }
+
+      $_arr = array_merge ($_arr, $value);
+    } else if ($key || $key === '' || $key === '0') {
+      if (!is_array ($_arr))
+        $_arr = array ($key=>$value);
+      else
+        $_arr[$key] = $value;
+    } else {
+      if (!is_array ($_arr)) { $_arr = array ($value); $key = 0; }
+      else { $_arr[] = $value; end ($_arr); $key = key ($_arr); }
+    }
+
+    $reverse_path = array_reverse($this->path);
+    $reverse_history = array_reverse ($history);
+    $reverse_history[0] = $_arr;
+    $cnt = count($reverse_history) - 1;
+    for ($i = 0; $i < $cnt; $i++) {
+      $reverse_history[$i+1][$reverse_path[$i]] = $reverse_history[$i];
+    }
+    $this->result = $reverse_history[$cnt];
+
+    $this->path[$incoming_indent] = $key;
+
+    if ($this->_containsGroupAnchor) {
+      $this->SavedGroups[$this->_containsGroupAnchor] = $this->path;
+      if (is_array ($value)) {
+        $k = key ($value);
+        if (!is_int ($k)) {
+          $this->SavedGroups[$this->_containsGroupAnchor][$incoming_indent + 2] = $k;
+        }
+      }
+      $this->_containsGroupAnchor = false;
+    }
+
+  }
+
+  function startsLiteralBlock ($line) {
+    $lastChar = substr (trim($line), -1);
+    if ($lastChar != '>' && $lastChar != '|') return false;
+    if ($lastChar == '|') return $lastChar;
+    // HTML tags should not be counted as literal blocks.
+    if (preg_match ('#<.*?>$#', $line)) return false;
+    return $lastChar;
+  }
+
+  function greedilyNeedNextLine($line) {
+    $line = trim ($line);
+    if (!strlen($line)) return false;
+    if (substr ($line, -1, 1) == ']') return false;
+    if ($line[0] == '[') return true;
+    if (preg_match ('#^[^:]+?:\s*\[#', $line)) return true;
+    return false;
+  }
+
+  function addLiteralLine ($literalBlock, $line, $literalBlockStyle, $indent = -1) {
+    $line = $this->stripIndent($line, $indent);
+    if ($literalBlockStyle !== '|') {
+        $line = $this->stripIndent($line);
+    }
+    $line = rtrim ($line, "\r\n\t ") . "\n";
+    if ($literalBlockStyle == '|') {
+      return $literalBlock . $line;
+    }
+    if (strlen($line) == 0)
+      return rtrim($literalBlock, ' ') . "\n";
+    if ($line == "\n" && $literalBlockStyle == '>') {
+      return rtrim ($literalBlock, " \t") . "\n";
+    }
+    if ($line != "\n")
+      $line = trim ($line, "\r\n ") . " ";
+    return $literalBlock . $line;
+  }
+
+   function revertLiteralPlaceHolder ($lineArray, $literalBlock) {
+     foreach ($lineArray as $k => $_) {
+      if (is_array($_))
+        $lineArray[$k] = $this->revertLiteralPlaceHolder ($_, $literalBlock);
+      else if (substr($_, -1 * strlen ($this->LiteralPlaceHolder)) == $this->LiteralPlaceHolder)
+	       $lineArray[$k] = rtrim ($literalBlock, " \r\n");
+     }
+     return $lineArray;
+   }
+
+  function stripIndent ($line, $indent = -1) {
+    if ($indent == -1) $indent = strlen($line) - strlen(ltrim($line));
+    return substr ($line, $indent);
+  }
+
+  function getParentPathByIndent ($indent) {
+    if ($indent == 0) return array();
+    $linePath = $this->path;
+    do {
+      end($linePath); $lastIndentInParentPath = key($linePath);
+      if ($indent <= $lastIndentInParentPath) array_pop ($linePath);
+    } while ($indent <= $lastIndentInParentPath);
+    return $linePath;
+  }
+
+
+  function clearBiggerPathValues ($indent) {
+
+
+    if ($indent == 0) $this->path = array();
+    if (empty ($this->path)) return true;
+
+    foreach ($this->path as $k => $_) {
+      if ($k > $indent) unset ($this->path[$k]);
+    }
+
+    return true;
+  }
+
+
+  function isComment ($line) {
+    if (!$line) return false;
+    if ($line[0] == '#') return true;
+    if (trim($line, " \r\n\t") == '---') return true;
+    return false;
+  }
+
+  function isEmpty ($line) {
+    return (trim ($line) === '');
+  }
+
+
+  function isArrayElement ($line) {
+    if (!$line) return false;
+    if ($line[0] != '-') return false;
+    if (strlen ($line) > 3)
+      if (substr($line,0,3) == '---') return false;
+
+    return true;
+  }
+
+  function isHashElement ($line) {
+    return strpos($line, ':');
+  }
+
+  function isLiteral ($line) {
+    if ($this->isArrayElement($line)) return false;
+    if ($this->isHashElement($line)) return false;
+    return true;
+  }
+
+
+  function unquote ($value) {
+    if (!$value) return $value;
+    if (!is_string($value)) return $value;
+    if ($value[0] == '\'') return trim ($value, '\'');
+    if ($value[0] == '"') return trim ($value, '"');
+    return $value;
+  }
+
+  function startsMappedSequence ($line) {
+    return ($line[0] == '-' && substr ($line, -1, 1) == ':');
+  }
+
+  function returnMappedSequence ($line) {
+    $array = array();
+    $key         = $this->unquote(trim(substr($line,1,-1)));
+    $array[$key] = array();
+    $this->delayedPath = array(strpos ($line, $key) + $this->indent => $key);
+    return array($array);
+  }
+
+  function returnMappedValue ($line) {
+    $array = array();
+    $key         = $this->unquote (trim(substr($line,0,-1)));
+    $array[$key] = '';
+    return $array;
+  }
+
+  function startsMappedValue ($line) {
+    return (substr ($line, -1, 1) == ':');
+  }
+
+  function isPlainArray ($line) {
+    return ($line[0] == '[' && substr ($line, -1, 1) == ']');
+  }
+
+  function returnPlainArray ($line) {
+    return $this->_toType($line);
+  }
+
+  function returnKeyValuePair ($line) {
+    $array = array();
+    $key = '';
+    if (strpos ($line, ':')) {
+      // It's a key/value pair most likely
+      // If the key is in double quotes pull it out
+      if (($line[0] == '"' || $line[0] == "'") && preg_match('/^(["\'](.*)["\'](\s)*:)/',$line,$matches)) {
+        $value = trim(str_replace($matches[1],'',$line));
+        $key   = $matches[2];
+      } else {
+        // Do some guesswork as to the key and the value
+        $explode = explode(':',$line);
+        $key     = trim($explode[0]);
+        array_shift($explode);
+        $value   = trim(implode(':',$explode));
+      }
+      // Set the type of the value.  Int, string, etc
+      $value = $this->_toType($value);
+      if ($key === '0') $key = '__!YAMLZero';
+      $array[$key] = $value;
+    } else {
+      $array = array ($line);
+    }
+    return $array;
+
+  }
+
+
+  function returnArrayElement ($line) {
+     if (strlen($line) <= 1) return array(array()); // Weird %)
+     $array = array();
+     $value   = trim(substr($line,1));
+     $value   = $this->_toType($value);
+     $array[] = $value;
+     return $array;
+  }
+
+
+  function nodeContainsGroup ($line) {
+    $symbolsForReference = 'A-z0-9_\-';
+    if (strpos($line, '&') === false && strpos($line, '*') === false) return false; // Please die fast ;-)
+    if ($line[0] == '&' && preg_match('/^(&['.$symbolsForReference.']+)/', $line, $matches)) return $matches[1];
+    if ($line[0] == '*' && preg_match('/^(\*['.$symbolsForReference.']+)/', $line, $matches)) return $matches[1];
+    if (preg_match('/(&['.$symbolsForReference.']+)$/', $line, $matches)) return $matches[1];
+    if (preg_match('/(\*['.$symbolsForReference.']+$)/', $line, $matches)) return $matches[1];
+    if (preg_match ('#^\s*<<\s*:\s*(\*[^\s]+).*$#', $line, $matches)) return $matches[1];
+    return false;
+
+  }
+
+  function addGroup ($line, $group) {
+    if ($group[0] == '&') $this->_containsGroupAnchor = substr ($group, 1);
+    if ($group[0] == '*') $this->_containsGroupAlias = substr ($group, 1);
+    //print_r ($this->path);
+  }
+
+  function stripGroup ($line, $group) {
+    $line = trim(str_replace($group, '', $line));
+    return $line;
+  }
+}
+
+// Enable use of Spyc from command line
+// The syntax is the following: php Spyc.php spyc.yaml
+
+do {
+  if (PHP_SAPI != 'cli') break;
+  if (empty ($_SERVER['argc']) || $_SERVER['argc'] < 2) break;
+  if (empty ($_SERVER['PHP_SELF']) || FALSE === strpos ($_SERVER['PHP_SELF'], 'Spyc.php') ) break;
+  $file = $argv[1];
+  echo json_encode (spyc_load_file ($file));
+} while (0);
diff --git a/include/ajax.content.php b/include/ajax.content.php
index 7ba1a1d4c31adfb467e87d01ed856b8e547c909a..197d75ffe50c622296ca0bb22335874773fb499b 100644
--- a/include/ajax.content.php
+++ b/include/ajax.content.php
@@ -15,9 +15,9 @@
 **********************************************************************/
 
 if(!defined('INCLUDE_DIR')) die('!');
-	    
+
 class ContentAjaxAPI extends AjaxController {
-   
+
     function log($id) {
 
         if($id && ($log=Log::lookup($id))) {
@@ -77,6 +77,8 @@ class ContentAjaxAPI extends AjaxController {
                     <tr><td>%{assignee}</td><td>Assigned staff/team</td></tr>
                     <tr><td>%{assigner}</td><td>Staff assigning the ticket</td></tr>
                     <tr><td>%{url}</td><td>osTicket\'s base url (FQDN)</td></tr>
+                    <tr><td>%{reset_link}</td>
+                        <td>Reset link used by the password reset feature</td></tr>
                 </table>
             </td>
         </tr>
diff --git a/include/class.config.php b/include/class.config.php
index a1dd1de245f0a5245396a2cf81369ced37f1e8eb..3726309876cf198d317e81e7988a7aa9f53476d5 100644
--- a/include/class.config.php
+++ b/include/class.config.php
@@ -20,11 +20,17 @@ class Config {
     var $config = array();
 
     var $section = null;                    # Default namespace ('core')
-    var $table = 'config';                  # Table name (with prefix)
+    var $table = CONFIG_TABLE;              # Table name (with prefix)
     var $section_column = 'namespace';      # namespace column name
 
     var $session = null;                    # Session-backed configuration
 
+    # Defaults for this configuration. If settings don't exist in the
+    # database yet, the ->getInfo() method will not include the (default)
+    # values in the returned array. $defaults allows developers to define
+    # new settings and the corresponding default values.
+    var $defaults = array();                # List of default values
+
     function Config($section=null) {
         if ($section)
             $this->section = $section;
@@ -36,7 +42,7 @@ class Config {
             $_SESSION['cfg:'.$this->section] = array();
         $this->session = &$_SESSION['cfg:'.$this->section];
 
-        $sql='SELECT id, `key`, value FROM '.$this->table
+        $sql='SELECT id, `key`, value, `updated` FROM '.$this->table
             .' WHERE `'.$this->section_column.'` = '.db_input($this->section);
 
         if(($res=db_query($sql)) && db_num_rows($res))
@@ -49,7 +55,7 @@ class Config {
     }
 
     function getInfo() {
-        $info = array();
+        $info = $this->defaults;
         foreach ($this->config as $key=>$setting)
             $info[$key] = $setting['value'];
         return $info;
@@ -62,6 +68,8 @@ class Config {
             return $this->config[$key]['value'];
         elseif ($default !== null)
             return $this->set($key, $default);
+        elseif (isset($this->defaults[$key]))
+            return $this->defaults[$key];
         return null;
     }
 
@@ -78,6 +86,13 @@ class Config {
         return true;
     }
 
+    function lastModified($key) {
+        if (isset($this->config[$key]))
+            return $this->config[$key]['updated'];
+        else
+            return false;
+    }
+
     function create($key, $value) {
         $sql = 'INSERT INTO '.$this->table
             .' SET `'.$this->section_column.'`='.db_input($this->section)
@@ -124,6 +139,11 @@ class OsticketConfig extends Config {
     var $alertEmail;  //Alert Email
     var $defaultSMTPEmail; //Default  SMTP Email
 
+    var $defaults = array(
+        'allow_pw_reset' =>     true,
+        'pw_reset_mins' =>      30,
+    );
+
     function OsticketConfig($section=null) {
         parent::Config($section);
 
@@ -454,6 +474,30 @@ class OsticketConfig extends Config {
         return ($this->get('staff_ip_binding'));
     }
 
+    /**
+     * Configuration: allow_pw_reset
+     *
+     * TRUE if the <a>Forgot my password</a> link and system should be
+     * enabled, and FALSE otherwise.
+     */
+    function allowPasswordReset() {
+        return $this->get('allow_pw_reset');
+    }
+
+    /**
+     * Configuration: pw_reset_window
+     *
+     * Number of minutes for which the password reset token is valid.
+     *
+     * Returns: Number of seconds the password reset token is valid. The
+     *      number of minutes from the database is automatically converted
+     *      to seconds here.
+     */
+    function getPwResetWindow() {
+        // pw_reset_window is stored in minutes. Return value in seconds
+        return $this->get('pw_reset_window') * 60;
+    }
+
     function isCaptchaEnabled() {
         return (extension_loaded('gd') && function_exists('gd_info') && $this->get('enable_captcha'));
     }
@@ -724,6 +768,8 @@ class OsticketConfig extends Config {
         $f['datetime_format']=array('type'=>'string',   'required'=>1, 'error'=>'Datetime format required');
         $f['daydatetime_format']=array('type'=>'string',   'required'=>1, 'error'=>'Day, Datetime format required');
         $f['default_timezone_id']=array('type'=>'int',   'required'=>1, 'error'=>'Default Timezone required');
+        $f['pw_reset_window']=array('type'=>'int', 'required'=>1, 'min'=>1,
+            'error'=>'Valid password reset window required');
 
 
         if(!Validator::process($f, $vars, $errors) || $errors)
@@ -746,6 +792,8 @@ class OsticketConfig extends Config {
             'client_max_logins'=>$vars['client_max_logins'],
             'client_login_timeout'=>$vars['client_login_timeout'],
             'client_session_timeout'=>$vars['client_session_timeout'],
+            'allow_pw_reset'=>isset($vars['allow_pw_reset'])?1:0,
+            'pw_reset_window'=>$vars['pw_reset_window'],
             'time_format'=>$vars['time_format'],
             'date_format'=>$vars['date_format'],
             'datetime_format'=>$vars['datetime_format'],
diff --git a/include/class.error.php b/include/class.error.php
new file mode 100644
index 0000000000000000000000000000000000000000..ed5bf7e4c3d8d0384c63f0d8a4dbaa7af8b543a3
--- /dev/null
+++ b/include/class.error.php
@@ -0,0 +1,60 @@
+<?php
+/*********************************************************************
+    class.error.php
+
+    Error handling for PHP < 5.0. Allows for returning a formal error from a
+    function since throwing it isn't available. Also allows for consistent
+    logging and debugging of errors in the osTicket system log.
+
+    Peter Rotich <peter@osticket.com>
+    Jared Hancock <jared@osticket.com>
+    Copyright (c)  2006-2013 osTicket
+    http://www.osticket.com
+
+    Released under the GNU General Public License WITHOUT ANY WARRANTY.
+    See LICENSE.TXT for details.
+
+    vim: expandtab sw=4 ts=4 sts=4:
+**********************************************************************/
+
+class Error /* extends Exception */ {
+    var $title = '';
+
+    function Error($message) {
+        call_user_func_array(array($this,'__construct'), func_get_args());
+    }
+    function __construct($message) {
+        global $ost;
+
+        $message = str_replace(ROOT_DIR, '(root)/', $message);
+
+        if ($ost->getConfig()->getLogLevel() == 3)
+            $message .= "\n\n" . $this->formatBacktrace(debug_backtrace());
+
+        $ost->logError($this->getTitle(), $message);
+    }
+
+    function getTitle() {
+        return get_class($this) . ": {$this->title}";
+    }
+
+    function formatBacktrace($bt) {
+        $buffer = array();
+        foreach ($bt as $i=>$frame)
+            $buffer[] = sprintf("#%d %s%s%s at [%s:%d]", $i,
+                $frame['class'], $frame['type'], $frame['function'],
+                str_replace(ROOT_DIR, '', $frame['file']), $frame['line']);
+        return implode("\n", $buffer);
+    }
+}
+
+class InitialDataError extends Error {
+    var $title = 'Problem with install initial data';
+}
+
+function raise_error($message, $class=false) {
+    if (!$class) $class = 'Error';
+    new $class($message);
+}
+
+?>
diff --git a/include/class.misc.php b/include/class.misc.php
index b6d9a673f6e311a085ce4ab07991e4c3b286f1f9..e913a8de0fdd052b0e9a87ec9693b720cc36e03b 100644
--- a/include/class.misc.php
+++ b/include/class.misc.php
@@ -14,26 +14,40 @@
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 class Misc {
-    
-	function randCode($len=8) {
-		return substr(strtoupper(base_convert(microtime(),10,16)),0,$len);
+
+	function randCode($count=8, $chars=false) {
+        $chars = $chars ? $chars
+            : 'abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
+        $data = '';
+        $m = strlen($chars) - 1;
+        for ($i=0; $i < $count; $i++)
+            $data .= $chars[mt_rand(0,$m)];
+        return $data;
 	}
-    
+
+    function __rand_seed($value=0) {
+        // Form a 32-bit figure for the random seed with the lower 16-bits
+        // the microseconds of the current time, and the upper 16-bits from
+        // received value
+        $seed = ((int) $value % 65535) << 16;
+        $seed += (int) ((double) microtime() * 1000000) % 65535;
+        mt_srand($seed);
+    }
+
     /* Helper used to generate ticket IDs */
     function randNumber($len=6,$start=false,$end=false) {
 
-        mt_srand ((double) microtime() * 1000000);
         $start=(!$len && $start)?$start:str_pad(1,$len,"0",STR_PAD_RIGHT);
         $end=(!$len && $end)?$end:str_pad(9,$len,"9",STR_PAD_RIGHT);
-        
+
         return mt_rand($start,$end);
     }
 
-    /* misc date helpers...this will go away once we move to php 5 */ 
+    /* misc date helpers...this will go away once we move to php 5 */
     function db2gmtime($var){
         global $cfg;
         if(!$var) return;
-        
+
         $dbtime=is_int($var)?$var:strtotime($var);
         return $dbtime-($cfg->getDBTZoffset()*3600);
     }
@@ -41,7 +55,7 @@ class Misc {
     //Take user time or gmtime and return db (mysql) time.
     function dbtime($var=null){
          global $cfg;
-             
+
         if(is_null($var) || !$var)
             $time=Misc::gmtime(); //gm time.
         else{ //user time to GM.
@@ -52,7 +66,7 @@ class Misc {
         //gm to db time
         return $time+($cfg->getDBTZoffset()*3600);
     }
-    
+
     /*Helper get GM time based on timezone offset*/
     function gmtime() {
         return time()-date('Z');
@@ -67,7 +81,7 @@ class Misc {
 
     //Current page
     function currentURL() {
-        
+
         $str = 'http';
         if ($_SERVER['HTTPS'] == 'on') {
             $str .='s';
@@ -78,7 +92,7 @@ class Misc {
             if (isset($_SERVER['QUERY_STRING'])) {
                 $_SERVER['REQUEST_URI'].='?'.$_SERVER['QUERY_STRING'];
             }
-        } 
+        }
         if ($_SERVER['SERVER_PORT']!=80) {
             $str .= $_SERVER['SERVER_NAME'].':'.$_SERVER['SERVER_PORT'].$_SERVER['REQUEST_URI'];
         } else {
@@ -92,7 +106,7 @@ class Misc {
         $hr =is_null($hr)?0:$hr;
         $min =is_null($min)?0:$min;
 
-        //normalize;    
+        //normalize;
         if($hr>=24)
             $hr=$hr%24;
         elseif($hr<0)
@@ -106,7 +120,7 @@ class Misc {
             $min=15;
         else
             $min=0;
-       
+
         ob_start();
         echo sprintf('<select name="%s" id="%s">',$name,$name);
         echo '<option value="" selected>Time</option>';
diff --git a/include/class.staff.php b/include/class.staff.php
index a4edb4f7abd7e9a104d5ba182f87ff74dc9d62e4..19bd71bf02da54c1ee1fd6c36ac356c32f0ca7aa 100644
--- a/include/class.staff.php
+++ b/include/class.staff.php
@@ -15,6 +15,7 @@
 **********************************************************************/
 include_once(INCLUDE_DIR.'class.ticket.php');
 include_once(INCLUDE_DIR.'class.dept.php');
+include_once(INCLUDE_DIR.'class.error.php');
 include_once(INCLUDE_DIR.'class.team.php');
 include_once(INCLUDE_DIR.'class.group.php');
 include_once(INCLUDE_DIR.'class.passwd.php');
@@ -401,6 +402,7 @@ class Staff {
 
     //Staff profile update...unfortunately we have to separate it from admin update to avoid potential issues
     function updateProfile($vars, &$errors) {
+        global $cfg;
 
         $vars['firstname']=Format::striptags($vars['firstname']);
         $vars['lastname']=Format::striptags($vars['lastname']);
@@ -437,7 +439,17 @@ class Staff {
             elseif($vars['passwd1'] && strcmp($vars['passwd1'], $vars['passwd2']))
                 $errors['passwd2']='Password(s) do not match';
 
-            if(!$vars['cpasswd'])
+            if (($rtoken = $_SESSION['_staff']['reset-token'])) {
+                $_config = new Config('pwreset');
+                if ($_config->get($rtoken) != $this->getId())
+                    $errors['err'] =
+                        'Invalid reset token. Logout and try again';
+                elseif (!($ts = $_config->lastModified($rtoken))
+                        && ($cfg->getPwResetWindow() < (time() - strtotime($ts))))
+                    $errors['err'] =
+                        'Invalid reset token. Logout and try again';
+            }
+            elseif(!$vars['cpasswd'])
                 $errors['cpasswd']='Current password required';
             elseif(!$this->cmp_passwd($vars['cpasswd']))
                 $errors['cpasswd']='Invalid current password!';
@@ -470,8 +482,10 @@ class Staff {
             .' ,default_paper_size='.db_input($vars['default_paper_size']);
 
 
-        if($vars['passwd1'])
+        if($vars['passwd1']) {
             $sql.=' ,change_passwd=0, passwdreset=NOW(), passwd='.db_input(Passwd::hash($vars['passwd1']));
+            $this->cancelResetTokens();
+        }
 
         $sql.=' WHERE staff_id='.db_input($this->getId());
 
@@ -576,7 +590,7 @@ class Staff {
     }
 
     function lookup($id) {
-        return ($id && is_numeric($id) && ($staff= new Staff($id)) && $staff->getId()==$id)?$staff:null;
+        return ($id && ($staff= new Staff($id)) && $staff->getId()) ? $staff : null;
     }
 
     function login($username, $passwd, &$errors, $strike=true) {
@@ -600,31 +614,10 @@ class Staff {
         if($errors) return false;
 
         if(($user=new StaffSession(trim($username))) && $user->getId() && $user->check_passwd($passwd)) {
-            //update last login && password reset stuff.
-            $sql='UPDATE '.STAFF_TABLE.' SET lastlogin=NOW() ';
-            if($user->isPasswdResetDue() && !$user->isAdmin())
-                $sql.=',change_passwd=1';
-            $sql.=' WHERE staff_id='.db_input($user->getId());
-            db_query($sql);
-            //Now set session crap and lets roll baby!
-            $_SESSION['_staff'] = array(); //clear.
-            $_SESSION['_staff']['userID'] = $username;
-            $user->refreshSession(); //set the hash.
-            $_SESSION['TZ_OFFSET'] = $user->getTZoffset();
-            $_SESSION['TZ_DST'] = $user->observeDaylight();
-
-            //Log debug info.
-            $ost->logDebug('Staff login',
-                    sprintf("%s logged in [%s]", $user->getUserName(), $_SERVER['REMOTE_ADDR'])); //Debug.
-
-            //Regenerate session id.
-            $sid=session_id(); //Current id
-            session_regenerate_id(TRUE);
-            //Destroy old session ID - needed for PHP version < 5.1.0 TODO: remove when we move to php 5.3 as min. requirement.
-            if(($session=$ost->getSession()) && is_object($session) && $sid!=session_id())
-                $session->destroy($sid);
+            self::_do_login($user, $username);
 
             Signal::send('auth.login.succeeded', $user);
+            $user->cancelResetTokens();
 
             return $user;
         }
@@ -651,6 +644,36 @@ class Staff {
         return false;
     }
 
+    function _do_login($user, $username) {
+        global $ost;
+
+        //update last login && password reset stuff.
+        $sql='UPDATE '.STAFF_TABLE.' SET lastlogin=NOW() ';
+        if($user->isPasswdResetDue() && !$user->isAdmin())
+            $sql.=',change_passwd=1';
+        $sql.=' WHERE staff_id='.db_input($user->getId());
+        db_query($sql);
+        //Now set session crap and lets roll baby!
+        $_SESSION['_staff'] = array(); //clear.
+        $_SESSION['_staff']['userID'] = $username;
+        $user->refreshSession(); //set the hash.
+        $_SESSION['TZ_OFFSET'] = $user->getTZoffset();
+        $_SESSION['TZ_DST'] = $user->observeDaylight();
+
+        //Log debug info.
+        $ost->logDebug('Staff login',
+                sprintf("%s logged in [%s]", $user->getUserName(), $_SERVER['REMOTE_ADDR'])); //Debug.
+
+        //Regenerate session id.
+        $sid=session_id(); //Current id
+        session_regenerate_id(TRUE);
+        //Destroy old session ID - needed for PHP version < 5.1.0 TODO: remove when we move to php 5.3 as min. requirement.
+        if(($session=$ost->getSession()) && is_object($session) && $sid!=session_id())
+            $session->destroy($sid);
+
+        return $user;
+    }
+
     function create($vars, &$errors) {
         if(($id=self::save(0, $vars, $errors)) && $vars['teams'] && ($staff=Staff::lookup($id))) {
             $staff->updateTeams($vars['teams']);
@@ -660,6 +683,43 @@ class Staff {
         return $id;
     }
 
+    function cancelResetTokens() {
+        // TODO: Drop password-reset tokens from the config table for
+        //       this user id
+        $sql = 'DELETE FROM '.CONFIG_TABLE.' WHERE `namespace`="pwreset"
+            AND `value`='.db_input($this->getId());
+        db_query($sql);
+        unset($_SESSION['_staff']['reset-token']);
+    }
+
+    function sendResetEmail() {
+        global $ost, $cfg;
+
+        if(!($tpl = $this->getDept()->getTemplate()))
+            $tpl= $ost->getConfig()->getDefaultTemplate();
+
+        $token = Misc::randCode(48); // 290-bits
+        if (!($template = $tpl->getMsgTemplate('staff.pwreset')))
+            return new Error('Unable to retrieve password reset email template');
+
+        $msg = $ost->replaceTemplateVariables($template->asArray(), array(
+            'url' => $ost->getConfig()->getBaseUrl(),
+            'token' => $token,
+            'reset_link' => sprintf(
+                "%s/scp/pwreset.php?token=%s",
+                $ost->getConfig()->getBaseUrl(),
+                $token),
+        ));
+
+        if(!($email=$cfg->getAlertEmail()))
+            $email =$cfg->getDefaultEmail();
+
+        $_config = new Config('pwreset');
+        $_config->set($token, $this->getId());
+
+        $email->send($this->getEmail(), $msg['subj'], $msg['body']);
+    }
+
     function save($id, $vars, &$errors) {
 
         $vars['username']=Format::striptags($vars['username']);
@@ -736,8 +796,9 @@ class Staff {
             .' ,signature='.db_input($vars['signature'])
             .' ,notes='.db_input($vars['notes']);
 
-        if($vars['passwd1'])
+        if($vars['passwd1']) {
             $sql.=' ,passwd='.db_input(Passwd::hash($vars['passwd1']));
+        }
 
         if(isset($vars['change_passwd']))
             $sql.=' ,change_passwd=1';
diff --git a/include/class.template.php b/include/class.template.php
index c85297d2fd5f23f1cfb8b1b0df82bfc852369478..fd068d7a86047a8f8e6ad1a657827985f2494f44 100644
--- a/include/class.template.php
+++ b/include/class.template.php
@@ -13,6 +13,7 @@
 
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
+require_once INCLUDE_DIR.'class.yaml.php';
 
 class EmailTemplateGroup {
 
@@ -56,6 +57,10 @@ class EmailTemplateGroup {
         'ticket.overdue'=>array(
             'name'=>'Overdue Ticket Alert',
             'desc'=>'Alert sent to staff on stale or overdue tickets.'),
+        'staff.pwreset' => array(
+            'name' => 'Staff Password Reset',
+            'desc' => 'Notice sent to staff with the password reset link.',
+            'default' => 'templates/staff.pwreset.txt'),
         );
 
     function EmailTemplateGroup($id){
@@ -108,6 +113,10 @@ class EmailTemplateGroup {
         return $this->isEnabled();
     }
 
+    function getLanguage() {
+        return 'en_US';
+    }
+
     function isInUse(){
         global $cfg;
 
@@ -140,6 +149,9 @@ class EmailTemplateGroup {
         if ($tpl=EmailTemplate::lookupByName($this->getId(), $name, $this))
             return $tpl;
 
+        if ($tpl=EmailTemplate::fromInitialData($name, $this))
+            return $tpl;
+
         $ost->logWarning('Template Fetch Error', "Unable to fetch '$name' template - id #".$this->getId());
         return false;
     }
@@ -330,11 +342,12 @@ class EmailTemplateGroup {
 class EmailTemplate {
 
     var $id;
+    var $ht;
     var $_group;
 
     function EmailTemplate($id, $group=null){
         $this->id=0;
-        $this->load($id);
+        if ($id) $this->load($id);
         if ($group) $this->_group = $group;
     }
 
@@ -466,5 +479,24 @@ class EmailTemplate {
     function lookup($id, $group=null) {
         return ($id && is_numeric($id) && ($t= new EmailTemplate($id, $group)) && $t->getId()==$id)?$t:null;
     }
+
+    /**
+     * Load the template from the initial_data directory. The format of the
+     * file should be free flow text. The first line is the subject and the
+     * rest of the file is the body.
+     */
+    function fromInitialData($name, $group=null) {
+        $templ = new EmailTemplate(0, $group);
+        $lang = ($group) ? $group->getLanguage() : 'en_US';
+        $info = YamlDataParser::load(I18N_DIR . "$lang/templates/$name.yaml");
+        if (isset($info['subject']) && isset($info['body'])) {
+            $templ->ht = $info;
+            return $templ;
+        }
+        raise_error("$lang/templates/$name.yaml: "
+            . 'Email templates must define both "subject" and "body" parts of the template',
+            'InitialDataError');
+        return false;
+    }
 }
 ?>
diff --git a/include/class.validator.php b/include/class.validator.php
index 0c76d0e199c079d3c9b0cb31599d5aa8784ada81..c0659d7535334d93adb71352491563eed9b1b686 100644
--- a/include/class.validator.php
+++ b/include/class.validator.php
@@ -3,7 +3,7 @@
     class.validator.php
 
     Input validation helper. This class contains collection of functions used for data validation.
-   
+
     Peter Rotich <peter@osticket.com>
     Copyright (c)  2006-2013 osTicket
     http://www.osticket.com
@@ -28,11 +28,11 @@ class Validator {
             $this->fields=$fields;
             return (true);
         endif;
-        
+
         return (false);
     }
-   
-   
+
+
     function validate($source,$userinput=true){
 
         $this->errors=array();
@@ -56,7 +56,7 @@ class Validator {
         foreach($this->fields as $k=>$field){
             if(!$field['required'] && !$this->input[$k]) //NOT required...and no data provided...
                 continue;
-            
+
             if($field['required'] && !isset($this->input[$k]) || (!$this->input[$k] && $field['type']!='int')){ //Required...and no data provided...
                 $this->errors[$k]=$field['error'];
                 continue;
@@ -67,7 +67,9 @@ class Validator {
             case 'int':
                 if(!is_numeric($this->input[$k]))
                      $this->errors[$k]=$field['error'];
-                break;  
+                elseif ($field['min'] && $this->input[$k] < $field['min'])
+                     $this->errors[$k]=$field['error'];
+                break;
             case 'double':
                 if(!is_numeric($this->input[$k]))
                     $this->errors[$k]=$field['error'];
@@ -114,7 +116,7 @@ class Validator {
                 break;
             case 'zipcode':
                 if(!is_numeric($this->input[$k]) || (strlen($this->input[$k])!=5))
-                    $this->errors[$k]=$field['error'];   
+                    $this->errors[$k]=$field['error'];
                 break;
             default://If param type is not set...or handle..error out...
                 $this->errors[$k]=$field['error'].' (type not set)';
@@ -122,15 +124,15 @@ class Validator {
         }
         return ($this->errors)?(FALSE):(TRUE);
     }
-   
+
     function iserror(){
         return $this->errors?true:false;
     }
-   
+
     function errors(){
         return $this->errors;
     }
-   
+
     /*** Functions below can be called directly without class instance. Validator::func(var..); ***/
     function is_email($email) {
         return preg_match('/^([*+!.&#$|\'\\%\/0-9a-z^_`{}=?~:-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i',$email);
@@ -140,17 +142,17 @@ class Validator {
         $stripped=preg_replace("(\(|\)|\-|\.|\+|[  ]+)","",$phone);
         return (!is_numeric($stripped) || ((strlen($stripped)<7) || (strlen($stripped)>16)))?false:true;
     }
-    
+
     function is_url($url) {
         //XXX: parse_url is not ideal for validating urls but it's ideal for basic checks.
         return ($url && ($info=parse_url($url)) && $info['host']);
     }
 
     function is_ip($ip) {
-      
+
         if(!$ip or empty($ip))
             return false;
-      
+
         $ip=trim($ip);
         # Thanks to http://stackoverflow.com/a/1934546
         if (function_exists('inet_pton')) { # PHP 5.1.0
diff --git a/include/class.yaml.php b/include/class.yaml.php
new file mode 100644
index 0000000000000000000000000000000000000000..d21605ca0b0cb04d178b36f38c7cf34cf0f5d253
--- /dev/null
+++ b/include/class.yaml.php
@@ -0,0 +1,41 @@
+<?php
+/*********************************************************************
+    class.yaml.php
+
+    Parses YAML data files into PHP associative arrays. Useful for initial
+    data shipped with osTicket.
+
+    Currently, this module uses the pure-php implementation Spyc, written by
+        - Chris Wanstrath
+        - Vlad Andersen
+    and released under an MIT license. The software is available at
+    https://github.com/mustangostang/spyc
+
+    Jared Hancock <jared@osticket.com>
+    Copyright (c)  2006-2013 osTicket
+    http://www.osticket.com
+
+    Released under the GNU General Public License WITHOUT ANY WARRANTY.
+    See LICENSE.TXT for details.
+
+    vim: expandtab sw=4 ts=4 sts=4:
+    $Id: $
+**********************************************************************/
+
+require_once "Spyc.php";
+
+class YamlDataParser {
+    /* static */
+    function load($file) {
+        if (!file_exists($file)) {
+            raise_error("$file: File does not exist", 'YamlParserError');
+            return false;
+        }
+        return Spyc::YAMLLoad($file);
+    }
+}
+
+class YamlParserError extends Error {
+    var $title = 'Error parsing YAML document';
+}
+?>
diff --git a/include/i18n/en_US/templates/staff.pwreset.yaml b/include/i18n/en_US/templates/staff.pwreset.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..2ed5b3da46bd6dc3ce7595d40243caf249db9591
--- /dev/null
+++ b/include/i18n/en_US/templates/staff.pwreset.yaml
@@ -0,0 +1,20 @@
+#
+# Email template: staff.pwreset
+#
+# Sent when a staff member requests a password reset via the <a>Forgot my
+# password</a> link in the staff control login screen
+#
+---
+subject: osTicket Staff Password Reset
+body: |
+    You or perhaps somebody you know has submitted a password reset request on
+    your behalf for the helpdesk at %{url}.
+
+    If you feel that this has been done in error. Delete and disregard this
+    email.  Your account is still secure and no one has been given access to it.
+    It is not locked and your password has not been reset. Someone could have
+    mistakenly entered your email address.
+
+    Follow the link below to login to the help desk and change your password.
+
+    %{reset_link}
diff --git a/include/mysql.php b/include/mysql.php
index 2a479072cf57eff4dcf6da1e2e7c026949c1a357..4e3bd7eb8caf242c92b3846fb1b7d5c845f31a87 100644
--- a/include/mysql.php
+++ b/include/mysql.php
@@ -23,6 +23,7 @@
       	    return NULL;
 
         //Connect
+        $start = (double) microtime() * 1000000;
         if(!($dblink =@mysql_connect($host, $user, $passwd)))
             return NULL;
 
@@ -36,6 +37,9 @@
 
         @db_set_variable('sql_mode', '');
 
+        // Use connection timing to seed the random number generator
+        Misc::__rand_seed(((double) microtime() * 1000000) - $start);
+
         return $dblink;
     }
 
diff --git a/include/mysqli.php b/include/mysqli.php
index ced95434a971b729e5934b3f5754f8f25166aad8..ec369c652accad3f33af04eb085ee0e1ab88a584 100644
--- a/include/mysqli.php
+++ b/include/mysqli.php
@@ -39,6 +39,7 @@ function db_connect($host, $user, $passwd, $options = array()) {
         return NULL;
 
     //Connectr
+    $start = microtime(true);
     if(!@$__db->real_connect($host, $user, $passwd))
         return NULL;
 
@@ -52,6 +53,9 @@ function db_connect($host, $user, $passwd, $options = array()) {
 
     @db_set_variable('sql_mode', '');
 
+    // Use connection timing to seed the random number generator
+    Misc::__rand_seed((microtime(true) - $start) * 1000000);
+
     return $__db;
 }
 
diff --git a/include/staff/login.header.php b/include/staff/login.header.php
new file mode 100644
index 0000000000000000000000000000000000000000..679a509f96baa7f581bf8463048d3b642531d51a
--- /dev/null
+++ b/include/staff/login.header.php
@@ -0,0 +1,22 @@
+<?php
+defined('OSTSCPINC') or die('Invalid path');
+?>
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+    <title>osTicket:: SCP Login</title>
+    <link rel="stylesheet" href="css/login.css" type="text/css" />
+    <meta name="robots" content="noindex" />
+    <meta http-equiv="cache-control" content="no-cache" />
+    <meta http-equiv="pragma" content="no-cache" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
+    <script type="text/javascript" src="../js/jquery-1.7.2.min.js"></script>
+    <script type="text/javascript">
+        $(document).ready(function() {
+            $("input:not(.dp):visible:enabled:first").focus();
+         });
+    </script>
+</head>
+<body id="loginBody">
+
diff --git a/include/staff/login.tpl.php b/include/staff/login.tpl.php
index b8b136eb56d01282e8b15dbd4c0f11085e437eaa..6d5435732128d556219c0230bdfb66004bdc21e7 100644
--- a/include/staff/login.tpl.php
+++ b/include/staff/login.tpl.php
@@ -1,26 +1,7 @@
-<?php 
-defined('OSTSCPINC') or die('Invalid path');
-
+<?php
+include_once(INCLUDE_DIR.'staff/login.header.php');
 $info = ($_POST && $errors)?Format::htmlchars($_POST):array();
 ?>
-<!DOCTYPE html>
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head>
-    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
-    <title>osTicket:: SCP Login</title>
-    <link rel="stylesheet" href="css/login.css" type="text/css" />
-    <meta name="robots" content="noindex" />
-    <meta http-equiv="cache-control" content="no-cache" />
-    <meta http-equiv="pragma" content="no-cache" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
-    <script type="text/javascript" src="../js/jquery-1.7.2.min.js"></script>
-    <script type="text/javascript">
-        $(document).ready(function() {
-            $("input:not(.dp):visible:enabled:first").focus();
-         });
-    </script>
-</head>
-<body id="loginBody">
 <div id="loginBox">
     <h1 id="logo"><a href="index.php">osTicket Staff Control Panel</a></h1>
     <h3><?php echo Format::htmlchars($msg); ?></h3>
@@ -28,9 +9,12 @@ $info = ($_POST && $errors)?Format::htmlchars($_POST):array();
         <?php csrf_token(); ?>
         <input type="hidden" name="do" value="scplogin">
         <fieldset>
-            <input type="text" name="username" id="name" value="<?php echo $info['username']; ?>" placeholder="username" autocorrect="off" autocapitalize="off">
+            <input type="text" name="userid" id="name" value="<?php echo $info['username']; ?>" placeholder="username" autocorrect="off" autocapitalize="off">
             <input type="password" name="passwd" id="pass" placeholder="password" autocorrect="off" autocapitalize="off">
         </fieldset>
+        <?php if ($_SESSION['_staff']['strikes'] > 1 && $cfg->allowPasswordReset()) { ?>
+        <h3 style="display:inline"><a href="pwreset.php">Forgot my password</a></h3>
+        <?php } ?>
         <input class="submit" type="submit" name="submit" value="Log In">
     </form>
 </div>
diff --git a/include/staff/profile.inc.php b/include/staff/profile.inc.php
index 073a7c8a44229e1b18bf7b7b4cc738d8689d8396..2543c80d1cfa68444199a894f117c1521774afb4 100644
--- a/include/staff/profile.inc.php
+++ b/include/staff/profile.inc.php
@@ -190,6 +190,7 @@ $info['id']=$staff->getId();
                 <em><strong>Password</strong>: To reset your password, provide your current password and a new password below.&nbsp;<span class="error">&nbsp;<?php echo $errors['passwd']; ?></span></em>
             </th>
         </tr>
+        <?php if (!isset($_SESSION['_staff']['reset-token'])) { ?>
         <tr>
             <td width="180">
                 Current Password:
@@ -199,6 +200,7 @@ $info['id']=$staff->getId();
                 &nbsp;<span class="error">&nbsp;<?php echo $errors['cpasswd']; ?></span>
             </td>
         </tr>
+        <?php } ?>
         <tr>
             <td width="180">
                 New Password:
diff --git a/include/staff/pwreset.login.php b/include/staff/pwreset.login.php
new file mode 100644
index 0000000000000000000000000000000000000000..6f93f1f0118093aefd5eb7b67568a560d2fb9066
--- /dev/null
+++ b/include/staff/pwreset.login.php
@@ -0,0 +1,26 @@
+<?php
+include_once(INCLUDE_DIR.'staff/login.header.php');
+defined('OSTSCPINC') or die('Invalid path');
+$info = ($_POST)?Format::htmlchars($_POST):array();
+?>
+
+<div id="loginBox">
+    <h1 id="logo"><a href="index.php">osTicket Staff Password Reset</a></h1>
+    <h3><?php echo Format::htmlchars($msg); ?></h3>
+
+    <form action="pwreset.php" method="post">
+        <?php csrf_token(); ?>
+        <input type="hidden" name="do" value="newpasswd"/>
+        <input type="hidden" name="token" value="<?php echo $_REQUEST['token']; ?>"/>
+        <fieldset>
+            <input type="text" name="userid" id="name" value="<?php echo
+                $info['userid']; ?>" placeholder="username or email"
+                autocorrect="off" autocapitalize="off"/>
+        </fieldset>
+        <input class="submit" type="submit" name="submit" value="Login"/>
+    </form>
+</div>
+
+<div id="copyRights">Copyright &copy; <a href='http://www.osticket.com' target="_blank">osTicket.com</a></div>
+</body>
+</html>
diff --git a/include/staff/pwreset.php b/include/staff/pwreset.php
new file mode 100644
index 0000000000000000000000000000000000000000..6aadeb2fcf10dbb308ed7ec3f548f4afd6c94ee8
--- /dev/null
+++ b/include/staff/pwreset.php
@@ -0,0 +1,25 @@
+<?php
+include_once(INCLUDE_DIR.'staff/login.header.php');
+defined('OSTSCPINC') or die('Invalid path');
+$info = ($_POST && $errors)?Format::htmlchars($_POST):array();
+?>
+
+<div id="loginBox">
+    <h1 id="logo"><a href="index.php">osTicket Staff Password Reset</a></h1>
+    <h3><?php echo Format::htmlchars($msg); ?></h3>
+    <form action="pwreset.php" method="post">
+        <?php csrf_token(); ?>
+        <input type="hidden" name="do" value="sendmail">
+        <fieldset>
+            <input type="text" name="userid" id="name" value="<?php echo
+                $info['userid']; ?>" placeholder="username" autocorrect="off"
+                autocapitalize="off">
+        </fieldset>
+        <input class="submit" type="submit" name="submit" value="Send Email"/>
+    </form>
+
+</div>
+
+<div id="copyRights">Copyright &copy; <a href='http://www.osticket.com' target="_blank">osTicket.com</a></div>
+</body>
+</html>
diff --git a/include/staff/pwreset.sent.php b/include/staff/pwreset.sent.php
new file mode 100644
index 0000000000000000000000000000000000000000..832b78ef57470c4045ed615c152dc228eb414e79
--- /dev/null
+++ b/include/staff/pwreset.sent.php
@@ -0,0 +1,22 @@
+<?php
+include_once(INCLUDE_DIR.'staff/login.header.php');
+defined('OSTSCPINC') or die('Invalid path');
+$info = ($_POST && $errors)?Format::htmlchars($_POST):array();
+?>
+
+<div id="loginBox">
+    <h1 id="logo"><a href="index.php">osTicket Staff Password Reset</a></h1>
+    <h3>A confirmation email has been sent</h3>
+    <h3 style="color:black;"><em>
+    A password reset email was sent to the email on file for your account.
+    Follow the link in the email to reset your password.
+    </em></h3>
+
+    <form action="index.php" method="get">
+        <input class="submit" type="submit" name="submit" value="Login"/>
+    </form>
+</div>
+
+<div id="copyRights">Copyright &copy; <a href='http://www.osticket.com' target="_blank">osTicket.com</a></div>
+</body>
+</html>
diff --git a/include/staff/settings-system.inc.php b/include/staff/settings-system.inc.php
index 6fade6d964b1fab6cca118fa0b8cff86841aa417..8915c8b4f87048d60cfba3a4d0fe3e2ca697dc62 100644
--- a/include/staff/settings-system.inc.php
+++ b/include/staff/settings-system.inc.php
@@ -112,7 +112,12 @@ $gmtime = Misc::gmtime();
                 </select>
             </td>
         </tr>
-        <tr><td>Password Reset Policy:</th>
+        <tr>
+            <th colspan="2">
+                <em><b>Authentication Settings</b></em>
+            </th>
+        </tr>
+        <tr><td>Password Change Policy:</th>
             <td>
                 <select name="passwd_reset_period">
                    <option value="0"> &mdash; None &mdash;</option>
@@ -126,10 +131,20 @@ $gmtime = Misc::gmtime();
                 &nbsp;<font class="error">&nbsp;<?php echo $errors['passwd_reset_period']; ?></font>
             </td>
         </tr>
-        <tr><td>Bind Staff Session to IP:</td>
+        <tr><td>Allow Password Resets:</th>
             <td>
-              <input type="checkbox" name="staff_ip_binding" <?php echo $config['staff_ip_binding']?'checked="checked"':''; ?>>
-              <em>(binds staff session to originating IP address upon login)</em>
+              <input type="checkbox" name="allow_pw_reset" <?php echo $config['allow_pw_reset']?'checked="checked"':''; ?>>
+              <em>Enables the <u>Forgot my password</u> link on the staff
+              control panel</em>
+            </td>
+        </tr>
+        <tr><td>Password Reset Window:</th>
+            <td>
+              <input type="text" name="pw_reset_window" size="6" value="<?php
+                    echo $config['pw_reset_window']; ?>">
+                Maximum time <em>in minutes</em> a password reset token can
+                be valid.
+                &nbsp;<font class="error">&nbsp;<?php echo $errors['pw_reset_window']; ?></font>
             </td>
         </tr>
         <tr><td>Staff Excessive Logins:</td>
@@ -182,6 +197,12 @@ $gmtime = Misc::gmtime();
                 &nbsp;Maximum idle time in minutes before a client must log in again (enter 0 to disable).
             </td>
         </tr>
+        <tr><td>Bind Staff Session to IP:</td>
+            <td>
+              <input type="checkbox" name="staff_ip_binding" <?php echo $config['staff_ip_binding']?'checked="checked"':''; ?>>
+              <em>(binds staff session to originating IP address upon login)</em>
+            </td>
+        </tr>
         <tr>
             <th colspan="2">
                 <em><b>Date and Time Options</b>: Please refer to <a href="http://php.net/date" target="_blank">PHP Manual</a> for supported parameters.</em>
diff --git a/include/staff/tpl.inc.php b/include/staff/tpl.inc.php
index 8c1ede75e5e4ed12092c08201d22a0aebeaa2cc2..c5954090dbd4935f5161851620dbed063c98b13c 100644
--- a/include/staff/tpl.inc.php
+++ b/include/staff/tpl.inc.php
@@ -6,15 +6,23 @@ if (is_a($template, EmailTemplateGroup)) {
     $id = 0;
     $tpl_id = $template->getId();
     $name = $template->getName();
+    $group = $template;
     $selected = $_REQUEST['code_name'];
     $action = 'implement';
     $extras = array('code_name'=>$selected, 'tpl_id'=>$tpl_id);
     $msgtemplates=$template->all_names;
+    // Attempt to lookup the default data if it is defined
+    $default = @$template->getMsgTemplate($selected);
+    if ($default) {
+        $info['subj'] = $default->getSubject();
+        $info['body'] = $default->getBody();
+    }
 } else {
     // Template edit
     $id = $template->getId();
     $tpl_id = $template->getTplId();
     $name = $template->getGroup()->getName();
+    $group = $template->getGroup();
     $selected = $template->getCodeName();
     $action = 'updatetpl';
     $extras = array();
@@ -33,7 +41,7 @@ $tpl=$msgtemplates[$info['tpl']];
     <select id="tpl_options" name="id" style="width:300px;">
         <option value="">&mdash; Select Setting Group &mdash;</option>
         <?php
-        foreach($template->getGroup()->getTemplates() as $cn=>$t) {
+        foreach($group->getTemplates() as $cn=>$t) {
             $nfo=$t->getDescription();
             if (!$nfo['name'])
                 continue;
@@ -41,6 +49,10 @@ $tpl=$msgtemplates[$info['tpl']];
             echo sprintf('<option value="%s" %s>%s</option>',
                     $t->getId(),$sel,$nfo['name']);
         }
+        if ($id == 0) { ?>
+            <option selected="selected" value="<?php echo $id; ?>"><?php
+            echo $msgtemplates[$selected]['name']; ?></option>
+        <?php }
         ?>
     </select>
     <input type="submit" value="Go">
diff --git a/main.inc.php b/main.inc.php
index e5d19c0a417ce78d71ccfdb5b3a1772bff9ec042..062c1d139c52ecee11cb000fa5d16afd03160631 100644
--- a/main.inc.php
+++ b/main.inc.php
@@ -70,6 +70,7 @@
     define('SETUP_DIR',INCLUDE_DIR.'setup/');
 
     define('UPGRADE_DIR', INCLUDE_DIR.'upgrader/');
+    define('I18N_DIR', INCLUDE_DIR.'i18n/');
 
     /*############## Do NOT monkey with anything else beyond this point UNLESS you really know what you are doing ##############*/
 
diff --git a/scp/login.php b/scp/login.php
index fcefaafd666660cdaf2d44c5e32e4b0bcfaeab37..2f3cf2236e9f4996bb10b94764fb6d0a14d99d22 100644
--- a/scp/login.php
+++ b/scp/login.php
@@ -24,7 +24,7 @@ $msg = $_SESSION['_staff']['auth']['msg'];
 $msg = $msg?$msg:'Authentication Required';
 if($_POST) {
     //$_SESSION['_staff']=array(); #Uncomment to disable login strikes.
-    if(($user=Staff::login($_POST['username'], $_POST['passwd'], $errors))){
+    if(($user=Staff::login($_POST['userid'], $_POST['passwd'], $errors))){
         $dest=($dest && (!strstr($dest,'login.php') && !strstr($dest,'ajax.php')))?$dest:'index.php';
         @header("Location: $dest");
         require_once('index.php'); //Just incase header is messed up.
diff --git a/scp/pwreset.php b/scp/pwreset.php
new file mode 100644
index 0000000000000000000000000000000000000000..a8efb2f6ef86715463398d01143d907a0313e10e
--- /dev/null
+++ b/scp/pwreset.php
@@ -0,0 +1,88 @@
+<?php
+/*********************************************************************
+    pwreset.php
+
+    Handles step 2, 3 and 5 of password resetting
+        1. Fail to login (2+ fail login attempts)
+        2. Visit password reset form and enter username or email
+        3. Receive an email with a link and follow it
+        4. Visit password reset form again, with the link
+        5. Enter the username or email address again and login
+        6. Password change is now required, user changes password and
+           continues on with the session
+
+    Peter Rotich <peter@osticket.com>
+    Jared Hancock <jared@osticket.com>
+    Copyright (c)  2006-2013 osTicket
+    http://www.osticket.com
+
+    Released under the GNU General Public License WITHOUT ANY WARRANTY.
+    See LICENSE.TXT for details.
+
+    vim: expandtab sw=4 ts=4 sts=4:
+**********************************************************************/
+require_once('../main.inc.php');
+if(!defined('INCLUDE_DIR')) die('Fatal Error. Kwaheri!');
+
+require_once(INCLUDE_DIR.'class.staff.php');
+require_once(INCLUDE_DIR.'class.csrf.php');
+
+$tpl = 'pwreset.php';
+if($_POST) {
+    if (!$ost->checkCSRFToken()) {
+        Http::response(400, 'Valid CSRF Token Required');
+        exit;
+    }
+    switch ($_POST['do']) {
+        case 'sendmail':
+            if (($staff=Staff::lookup($_POST['userid']))) {
+                if (!$staff->sendResetEmail()) {
+                    $tpl = 'pwreset.sent.php';
+                }
+            }
+            else
+                $msg = 'Unable to verify username '
+                    .Format::htmlchars($_POST['userid']);
+            break;
+        case 'newpasswd':
+            // TODO: Compare passwords
+            $tpl = 'pwreset.login.php';
+            $_config = new Config('pwreset');
+            if (($staff = new StaffSession($_POST['userid'])) &&
+                    !$staff->getId())
+                $msg = 'Invalid user-id given';
+            elseif (!($id = $_config->get($_POST['token']))
+                    || $id != $staff->getId())
+                $msg = 'Invalid reset token';
+            elseif (!($ts = $_config->lastModified($_POST['token']))
+                    && ($ost->getConfig()->getPwResetWindow() < (time() - strtotime($ts))))
+                $msg = 'Invalid reset token';
+            elseif (!$staff->forcePasswdRest())
+                $msg = 'Unable to reset password';
+            else {
+                Staff::_do_login($staff, $_POST['userid']);
+                $_SESSION['_staff']['reset-token'] = $_POST['token'];
+                header('Location: index.php');
+                exit();
+            }
+            break;
+    }
+}
+elseif ($_GET['token']) {
+    $msg = 'Re-enter your username or email';
+    $_config = new Config('pwreset');
+    if (($id = $_config->get($_GET['token']))
+            && ($staff = Staff::lookup($id)))
+        $tpl = 'pwreset.login.php';
+    else
+        header('Location: index.php');
+}
+elseif ($cfg->allowPasswordReset()) {
+    $msg = 'Enter your username or email address below';
+}
+else {
+    $_SESSION['_staff']['auth']['msg']='Password resets are disabled';
+    return header('Location: index.php');
+}
+define("OSTSCPINC",TRUE); //Make includes happy!
+include_once(INCLUDE_DIR.'staff/'. $tpl);
diff --git a/scp/staff.inc.php b/scp/staff.inc.php
index 577fdd12c6a124a0d98dd50b792d080ab40f983e..503c3cd413be64319882c255f9595029cbd0615d 100644
--- a/scp/staff.inc.php
+++ b/scp/staff.inc.php
@@ -1,7 +1,7 @@
 <?php
 /*************************************************************************
     staff.inc.php
-    
+
     File included on every staff page...handles logins (security) and file path issues.
 
     Peter Rotich <peter@osticket.com>
@@ -42,13 +42,14 @@ require_once(INCLUDE_DIR.'class.nav.php');
 require_once(INCLUDE_DIR.'class.csrf.php');
 
 /* First order of the day is see if the user is logged in and with a valid session.
-    * User must be valid staff beyond this point 
+    * User must be valid staff beyond this point
     * ONLY super admins can access the helpdesk on offline state.
 */
 
 
 if(!function_exists('staffLoginPage')) { //Ajax interface can pre-declare the function to  trap expired sessions.
     function staffLoginPage($msg) {
+        global $ost, $cfg;
         $_SESSION['_staff']['auth']['dest']=THISURI;
         $_SESSION['_staff']['auth']['msg']=$msg;
         require(SCP_DIR.'login.php');
@@ -59,7 +60,15 @@ if(!function_exists('staffLoginPage')) { //Ajax interface can pre-declare the fu
 $thisstaff = new StaffSession($_SESSION['_staff']['userID']); //Set staff object.
 //1) is the user Logged in for real && is staff.
 if(!$thisstaff || !is_object($thisstaff) || !$thisstaff->getId() || !$thisstaff->isValid()){
-    $msg=(!$thisstaff || !$thisstaff->isValid())?'Authentication Required':'Session timed out due to inactivity';
+    if (isset($_SESSION['_staff']['auth']['msg'])) {
+        $msg = $_SESSION['_staff']['auth']['msg'];
+        unset($_SESSION['_staff']['auth']['msg']);
+    }
+    elseif ($thisstaff && !$thisstaff->isValid())
+        $msg = 'Session timed out due to inactivity';
+    else
+        $msg = 'Authentication Required';
+
     staffLoginPage($msg);
     exit;
 }
@@ -88,7 +97,7 @@ if ($_POST  && !$ost->checkCSRFToken()) {
     exit;
 }
 
-//Add token to the header - used on ajax calls [DO NOT CHANGE THE NAME] 
+//Add token to the header - used on ajax calls [DO NOT CHANGE THE NAME]
 $ost->addExtraHeader('<meta name="csrf_token" content="'.$ost->getCSRFToken().'" />');
 
 /******* SET STAFF DEFAULTS **********/
diff --git a/setup/test/tests/class.test.php b/setup/test/tests/class.test.php
index 542d47c353cab0314028f77a461d80b02cc34bfb..a9da96b2d50e547eebd768b58de21df2a53c30be 100644
--- a/setup/test/tests/class.test.php
+++ b/setup/test/tests/class.test.php
@@ -9,6 +9,7 @@ class Test {
         '/include/htmLawed.php',
         '/include/PasswordHash.php',
         '/include/pear/',
+        '/include/Spyc.php',
     );
 
     function Test() {