diff --git a/include/class.signal.php b/include/class.signal.php
new file mode 100644
index 0000000000000000000000000000000000000000..ac7a6a6388bc4f3f9018f23abfca12cb53575a9a
--- /dev/null
+++ b/include/class.signal.php
@@ -0,0 +1,104 @@
+<?php
+/*********************************************************************
+    class.signal.php
+
+    Simple interface for a publish and subscribe signal model
+
+    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:
+**********************************************************************/
+
+/**
+ * Signals implement a simple publish/subscribe event model. To keep things
+ * simplistic between classes and to maintain compatible with PHP version 4,
+ * signals will not be explicitly defined or registered. Instead, signals
+ * are connected to callbacks via a string signal name.
+ *
+ * The system is proofed with a static inspection test which will ensure
+ * that for every given Signal::connect() function call, somewhere else in
+ * the codebase there exists a Signal::send() for the same named signal.
+ */
+class Signal {
+    /**
+     * Subscribe to a signal.
+     *
+     * Signal::connect('user.auth', 'function');
+     *
+     * The subscribed function should receive a two arguments and will have
+     * this signature:
+     *
+     * function callback($object, $data);
+     *
+     * Where the $object argument is the object originating the signal, and
+     * the $options is a hash-array of other information originating from-
+     * and pertaining to the signal.
+     *
+     * The value of the $data argument is not defined. It is signal
+     * specific. It should be a hash-array of data; however, no runtime
+     * checks are made to ensure such an interface.
+     *
+     * Optionally, if $object is a class and is passed into the ::connect()
+     * method, only instances of the named class or subclass will actually
+     * be connected to the callable function.
+     *
+     * A predicate function, $check, can be used to filter calls to the
+     * signal handler. The function will receive the signal data and should
+     * return true if the signal handler should be called.
+     */
+    /*static*/ function connect($signal, $callable, $object=null,
+            $check=null) {
+        global $_subscribers;
+        if (!isset($_subscribers[$signal])) $_subscribers[$signal] = array();
+        // XXX: Ensure $object if set is a class
+        if ($object && !is_string($object))
+            trigger_error("Invalid object: $object: Expected class");
+        elseif ($check && !is_callable($check)) {
+            trigger_error("Invalid check function: Must be callable");
+            $check = null;
+        }
+        $_subscribers[$signal][] = array($object, $callable, $check);
+    }
+
+    /**
+     * Publish a signal.
+     *
+     * Signal::send('user.login', $this, array('username'=>'blah'));
+     *
+     * All subscribers to the signal will be called in the order they
+     * connect()ed to the signal. Subscribers do not have the opportunity to
+     * interrupt or discontinue delivery of the signal to other subscribers.
+     * The $object argument is required and should almost always be ($this).
+     * Its interpretation is the object originating or sending the signal.
+     * It could also be interpreted as the context of the signal.
+     *
+     * $data if sent should be a hash-array of data included with the signal
+     * event. There is otherwise no definition for what should or could be
+     * included in the $data array. The received data is received by
+     * reference and can be passed to the callable by reference if the
+     * callable is defined to receive it by reference. Therefore, it is
+     * possible to propogate changes in the signal handlers back to the
+     * originating context.
+     */
+    /*static*/ function send($signal, $object, &$data=null) {
+        global $_subscribers;
+        if (!isset($_subscribers[$signal]))
+            return;
+        foreach ($_subscribers[$signal] as $sub) {
+            list($s, $callable, $check) = $sub;
+            if ($s && !is_a($object, $s))
+                continue;
+            elseif ($check && !call_user_func($check, $data))
+                continue;
+            call_user_func($callable, $data);
+        }
+    }
+}
+
+$_subscribers = array();
+?>
diff --git a/include/class.staff.php b/include/class.staff.php
index 414c835cccd07522626eef9ab0f2f9fa573da32d..4dc2f6bf4b1724d16b067be77a3244baad387d49 100644
--- a/include/class.staff.php
+++ b/include/class.staff.php
@@ -501,6 +501,8 @@ class Staff {
 
         $this->updateTeams($vars['teams']);
         $this->reload();
+
+        Signal::send('model.modified', $this);
         
         return true;
     }
@@ -520,6 +522,8 @@ class Staff {
             db_query('DELETE FROM '.TEAM_MEMBER_TABLE.' WHERE staff_id='.db_input($id));
         }
 
+        Signal::send('model.deleted', $this);
+
         return $num;
     }
 
@@ -613,9 +617,14 @@ class Staff {
             //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);
+
+            Signal::send('auth.login.succeeded', $user);
         
             return $user;
         }
+
+        Signal::send('auth.login.failed', null, array('username'=>$username,
+            'password'=>$passwd));
     
         //If we get to this point we know the login failed.
         $_SESSION['_staff']['strikes']+=1;
@@ -637,8 +646,10 @@ class Staff {
     }
 
     function create($vars, &$errors) {
-        if(($id=self::save(0, $vars, $errors)) && $vars['teams'] && ($staff=Staff::lookup($id)))
+        if(($id=self::save(0, $vars, $errors)) && $vars['teams'] && ($staff=Staff::lookup($id))) {
             $staff->updateTeams($vars['teams']);
+            Signal::send('model.created', $staff);
+        }
 
         return $id;
     }
diff --git a/main.inc.php b/main.inc.php
index 69ec71bde3045b4f22d0c7ffef5d93f01621f742..d7f719c5da3ca5c9d45316157ad46f3ef33367d2 100644
--- a/main.inc.php
+++ b/main.inc.php
@@ -118,6 +118,7 @@
     require(INCLUDE_DIR.'class.misc.php');
     require(INCLUDE_DIR.'class.timezone.php');
     require(INCLUDE_DIR.'class.http.php');
+    require(INCLUDE_DIR.'class.signal.php');
     require(INCLUDE_DIR.'class.nav.php');
     require(INCLUDE_DIR.'class.format.php'); //format helpers
     require(INCLUDE_DIR.'class.validator.php'); //Class to help with basic form input validation...please help improve it.
diff --git a/setup/test/lint.php b/setup/test/lint.php
deleted file mode 100644
index 4887856182e23d3acfed19168a92dbc744f4e9e8..0000000000000000000000000000000000000000
--- a/setup/test/lint.php
+++ /dev/null
@@ -1,140 +0,0 @@
-#!/usr/bin/env php
-<?php
-if (php_sapi_name() != 'cli') exit();
-
-function get_osticket_root_path() {
-    # Hop up to the root folder
-    $start = dirname(__file__);
-    for (;;) {
-        if (file_exists($start . '/main.inc.php')) break;
-        $start .= '/..';
-    }
-    return realpath($start);
-}
-
-$root = get_osticket_root_path();
-
-# Check PHP syntax across all php files
-function glob_recursive($pattern, $flags = 0) {
-    $files = glob($pattern, $flags);
-    foreach (glob(dirname($pattern).'/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir) {
-        $files = array_merge($files, 
-            glob_recursive($dir.'/'.basename($pattern), $flags));
-    }
-    return $files;
-}
-echo "PHP Syntax Errors: ";
-ob_start();
-$scripts=glob_recursive("$root/*.php");
-$exit=0;
-$syntax_errors="";
-foreach ($scripts as $s) {
-    system("php -l $s", $exit);
-    $line = ob_get_contents();
-    ob_clean();
-    if ($exit !== 0)
-        $syntax_errors .= $line;
-}
-ob_end_clean();
-
-if (strlen($syntax_errors)) {
-    $syntax_errors=str_replace("$root/", '', $syntax_errors);
-    echo "FAIL\n";
-    echo "-------------------------------------------------------\n";
-    echo "$syntax_errors";
-    exit();
-} else {
-    echo "pass\n";
-}
-
-function line_number_for_offset($filename, $offset) {
-    $lines = file($filename);
-    $bytes = $line = 0;
-    while ($bytes < $offset) {
-        $bytes += strlen(array_shift($lines));
-        $line += 1;
-    }
-    return $line;
-}
-echo "Short open tags: ";
-$fails = array();
-foreach ($scripts as $s) {
-    $matches = array();
-    if (preg_match_all('/<\?\s*(?!php|xml).*$/m', file_get_contents($s), $matches,
-            PREG_OFFSET_CAPTURE) > 0) {
-        foreach ($matches[0] as $match)
-            $fails[] = array(
-                str_replace($root.'/', '', $s),
-                $match[0],
-                line_number_for_offset($s, $match[1]));
-    }
-}
-if (count($fails)) {
-    echo "FAIL\n";
-    echo "-------------------------------------------------------\n";
-    foreach ($fails as $f)
-        echo sprintf("In %s, line %d: %s\n", $f[0], $f[2],
-            str_replace("\n", " ", $f[1]));
-    echo "\n";
-} else {
-    echo "pass\n";
-}
-
-# Run phplint across all php files
-echo "Access to unitialized variables: ";
-ob_start();
-# XXX: This won't run well on Windoze
-system("$root/setup/test/lib/phplint.tcl ".implode(" ", $scripts));
-$lint_errors = ob_get_clean();
-
-if (strlen($lint_errors)) {
-    $lint_errors=str_replace("$root/", '', $lint_errors);
-    echo "FAIL\n";
-    echo "-------------------------------------------------------\n";
-    echo "$lint_errors";
-} else {
-    echo "\n";
-}
-
-function find_function_calls($scripts) {
-    $calls=array();
-    foreach ($scripts as $s) {
-        $lines = explode("\n", file_get_contents($s));
-        $lineno=0;
-        foreach (explode("\n", file_get_contents($s)) as $line) {
-            $lineno++; $matches=array();
-            preg_match_all('/-[>]([a-zA-Z0-9]*)\(/', $line, $matches,
-                PREG_SET_ORDER);
-            foreach ($matches as $m) {
-                $calls[] = array($s, $lineno, $line, $m[1]);
-            }
-        }
-    }
-    return $calls;
-}
-
-$php_script_content='';
-foreach ($scripts as $s) {
-    $php_script_content .= file_get_contents($s);
-}
-echo "Access to undefined object methods: ";
-ob_start();
-foreach (find_function_calls($scripts) as $call) {
-    list($file, $no, $line, $func) = $call;
-    if (!preg_match('/^\s*(\/\*[^*]*\*\/)?'."\s*function\s+&?\s*$func\\(/m", 
-            $php_script_content)) {
-        print "$func: Definitely undefined, from $file:$no\n";
-    }
-}
-$undef_func_errors = ob_get_clean();
-
-if (strlen($undef_func_errors)) {
-    $undef_func_errors=str_replace("$root/", '', $undef_func_errors);
-    echo "FAIL\n";
-    echo "-------------------------------------------------------\n";
-    echo "$undef_func_errors";
-    exit();
-} else {
-    echo "\n";
-}
-?>
diff --git a/setup/test/run-tests.php b/setup/test/run-tests.php
new file mode 100644
index 0000000000000000000000000000000000000000..45f8859a3be5f72d6853d862575ff1333966495b
--- /dev/null
+++ b/setup/test/run-tests.php
@@ -0,0 +1,52 @@
+#!/usr/bin/env php
+<?php
+if (php_sapi_name() != 'cli') exit();
+
+require_once "tests/class.test.php";
+
+function get_osticket_root_path() {
+    # Hop up to the root folder
+    $start = dirname(__file__);
+    for (;;) {
+        if (file_exists($start . '/main.inc.php')) break;
+        $start .= '/..';
+    }
+    return realpath($start);
+}
+$root = get_osticket_root_path();
+
+# Check PHP syntax across all php files
+function glob_recursive($pattern, $flags = 0) {
+    $files = glob($pattern, $flags);
+    foreach (glob(dirname($pattern).'/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir) {
+        $files = array_merge($files, 
+            glob_recursive($dir.'/'.basename($pattern), $flags));
+    }
+    return $files;
+}
+
+$fails = array();
+foreach (glob_recursive(dirname(__file__)."/tests/test.*.php") as $t) {
+    if (strpos($t,"class.") !== false)
+        continue;
+    $class = (include $t);
+    if (!is_string($class))
+        continue;
+    $test = new $class();
+    echo "Running: " . $test->name . "\n";
+    $test->run();
+    $fails = array_merge($fails, $test->fails);
+    echo " ok\n";
+}
+
+if ($fails) {
+    echo count($fails) . " FAIL(s)\n";
+    echo "-------------------------------------------------------\n";
+    foreach ($fails as $f) {
+        list($test, $script, $line, $message) = $f;
+        $script = str_replace($root.'/', '', $script);
+        print("$test: $message @ $script:$line\n");
+    }
+}
+
+?>
diff --git a/setup/test/tests/class.test.php b/setup/test/tests/class.test.php
new file mode 100644
index 0000000000000000000000000000000000000000..f9cfeb30911d4f688fef4edfab00a29cd54fd48b
--- /dev/null
+++ b/setup/test/tests/class.test.php
@@ -0,0 +1,66 @@
+<?php
+
+class Test {
+    var $fails = array();
+    var $name = "";
+
+    var $third_party_paths = array(
+        '/include/JSON.php',
+        '/include/htmLawed.php',
+        '/include/PasswordHash.php',
+        '/include/pear/',
+    );
+
+    function Test() {
+        call_user_func_array(array($this, '__construct'), func_get_args());
+    }
+    function __construct() {
+        assert_options(ASSERT_CALLBACK, array($this, 'fail'));
+        error_reporting(E_ALL & ~E_WARNING);
+    }
+
+    function setup() {
+    }
+
+    function teardown() {
+    }
+
+    /*static*/ function getAllScripts() {
+        $root = get_osticket_root_path();
+        $scripts = array();
+        foreach (glob_recursive("$root/*.php") as $s) {
+            $found = false;
+            foreach ($this->third_party_paths as $p) {
+                if (strpos($s, $p) !== false) {
+                    $found = true;
+                    break;
+                }
+            }
+            if (!$found)
+                $scripts[] = $s;
+        }
+        return $scripts;
+    }
+
+    function fail($script, $line, $message) {
+        $this->fails[] = array(get_class($this), $script, $line, $message);
+        fputs(STDOUT, 'F');
+    }
+
+    function pass() {
+        fputs(STDOUT, ".");
+    }
+
+    function run() {
+        $rc = new ReflectionClass(get_class($this));
+        foreach ($rc->getMethods() as $m) {
+            if (stripos($m->name, 'test') === 0) {
+                $this->setup();
+                call_user_func(array($this, $m->name));
+                $this->teardown();
+            }
+        }
+    }
+
+}
+?>
diff --git a/setup/test/lib/phplint.tcl b/setup/test/tests/lib/phplint.tcl
similarity index 100%
rename from setup/test/lib/phplint.tcl
rename to setup/test/tests/lib/phplint.tcl
diff --git a/setup/test/tests/test.shortopentags.php b/setup/test/tests/test.shortopentags.php
new file mode 100644
index 0000000000000000000000000000000000000000..aab8e0156a388ac93f9d7bede23adb8da8c392bb
--- /dev/null
+++ b/setup/test/tests/test.shortopentags.php
@@ -0,0 +1,35 @@
+<?php
+require_once "class.test.php";
+
+class ShortOpenTag extends Test {
+    var $name = "PHP Short Open Checks";
+
+    function testFindShortOpens() {
+        foreach ($this->getAllScripts() as $s) {
+            $matches = array();
+            if (preg_match_all('/<\?\s*(?!php|xml).*$/m',
+                    file_get_contents($s), $matches,
+                    PREG_OFFSET_CAPTURE) > 0) {
+                foreach ($matches[0] as $match)
+                    $this->fail(
+                        $s,
+                        line_number_for_offset($s, $match[1]),
+                        $match[0]);
+            }
+            else $this->pass();
+        }
+    }
+}
+
+function line_number_for_offset($filename, $offset) {
+    $lines = file($filename);
+    $bytes = $line = 0;
+    while ($bytes < $offset) {
+        $bytes += strlen(array_shift($lines));
+        $line += 1;
+    }
+    return $line;
+}
+
+return 'ShortOpenTag';
+?>
diff --git a/setup/test/tests/test.signals.php b/setup/test/tests/test.signals.php
new file mode 100644
index 0000000000000000000000000000000000000000..7ce888383ab0aee781450f3f908d8c4a36d96ae1
--- /dev/null
+++ b/setup/test/tests/test.signals.php
@@ -0,0 +1,47 @@
+<?php
+require_once "class.test.php";
+
+class SignalsTest extends Test {
+    var $name = "Signals checks";
+
+    /**
+     * Ensures that each signal subscribed to has a sender somewhere else
+     */
+    function testFindSignalPublisher() {
+        $scripts = $this->getAllScripts();
+        $matches = $published_signals = array();
+        foreach ($scripts as $s)
+            if (preg_match_all("/^ *Signal::send\('([^']+)'/m",
+                    file_get_contents($s), $matches, PREG_SET_ORDER))
+                foreach ($matches as $match)
+                    $published_signals[] = $match[1];
+        foreach ($scripts as $s) {
+            if (preg_match_all("/^ *Signal::connect\('([^']+)'/m",
+                    file_get_contents($s), $matches,
+                    PREG_OFFSET_CAPTURE|PREG_SET_ORDER) > 0) {
+                foreach ($matches as $match) {
+                    $match = $match[1];
+                    if (!in_array($match[0], $published_signals))
+                        $this->fail(
+                            $s, self::line_number_for_offset($s, $match[1]),
+                            "Signal '{$match[0]}' is never sent");
+                    else
+                        $this->pass();
+                }
+            }
+        }
+    }
+
+    function line_number_for_offset($filename, $offset) {
+        $lines = file($filename);
+        $bytes = $line = 0;
+        while ($bytes < $offset) {
+            $bytes += strlen(array_shift($lines));
+            $line += 1;
+        }
+        return $line;
+    }
+}
+
+return 'SignalsTest';
+?>
diff --git a/setup/test/tests/test.syntax.php b/setup/test/tests/test.syntax.php
new file mode 100644
index 0000000000000000000000000000000000000000..774ed964e466a20ce63b352fe7792d68769513d1
--- /dev/null
+++ b/setup/test/tests/test.syntax.php
@@ -0,0 +1,23 @@
+<?php
+require_once "class.test.php";
+
+class SyntaxTest extends Test {
+    var $name = "PHP Syntax Checks";
+
+    function testCompileErrors() {
+        $exit = 0;
+        foreach ($this->getAllScripts() as $s) {
+            ob_start();
+            system("php -l $s", $exit);
+            $line = ob_get_contents();
+            ob_end_clean();
+            if ($exit != 0)
+                $this->fail($s, 0, $line);
+            else
+                $this->pass();
+        }
+    }
+}
+
+return 'SyntaxTest';
+?>
diff --git a/setup/test/tests/test.undefinedmethods.php b/setup/test/tests/test.undefinedmethods.php
new file mode 100644
index 0000000000000000000000000000000000000000..7062ec256b9b062b7858c1b94c0e50e82a3c5f0f
--- /dev/null
+++ b/setup/test/tests/test.undefinedmethods.php
@@ -0,0 +1,42 @@
+<?php
+require_once "class.test.php";
+
+class UndefinedMethods extends Test {
+    var $name = "Access to undefined object methods";
+
+    function testFindShortOpen() {
+        $scripts = $this->getAllScripts();
+        $php_script_content='';
+        foreach ($scripts as $s) {
+            $php_script_content .= file_get_contents($s);
+        }
+        foreach (find_function_calls($scripts) as $call) {
+            list($file, $no, $line, $func) = $call;
+            if (!preg_match('/^\s*(\/\*[^*]*\*\/)?'."\s*function\s+&?\s*$func\\(/m", 
+                    $php_script_content))
+                $this->fail($file, $no, "$func: Definitely undefined");
+            else
+                $this->pass();
+        }
+    }
+}
+
+function find_function_calls($scripts) {
+    $calls=array();
+    foreach ($scripts as $s) {
+        $lines = explode("\n", file_get_contents($s));
+        $lineno=0;
+        foreach ($lines as $line) {
+            $lineno++; $matches=array();
+            preg_match_all('/-[>]([a-zA-Z0-9]*)\(/', $line, $matches,
+                PREG_SET_ORDER);
+            foreach ($matches as $m) {
+                $calls[] = array($s, $lineno, $line, $m[1]);
+            }
+        }
+    }
+    return $calls;
+}
+
+return 'UndefinedMethods';
+?>
diff --git a/setup/test/tests/test.unitialized.php b/setup/test/tests/test.unitialized.php
new file mode 100644
index 0000000000000000000000000000000000000000..09bd0509e155072ccb6044429248965c2e318b36
--- /dev/null
+++ b/setup/test/tests/test.unitialized.php
@@ -0,0 +1,25 @@
+<?php
+require_once "class.test.php";
+
+class UnitializedVars extends Test {
+    var $name = "Access to unitialized variables";
+
+    function testUnitializedUsage() {
+        $scripts = $this->getAllScripts();
+        $matches = array();
+        foreach (range(0, count($scripts), 40) as $start) {
+            $slice = array_slice($scripts, $start, 40);
+            ob_start();
+            # XXX: This won't run well on Windoze
+            system(dirname(__file__)."/lib/phplint.tcl ".implode(" ", $slice));
+            $lint_errors = ob_get_clean();
+            preg_match_all("/\* In (.*) line (\d+): access to uninitialized var '([^']+)'/m",
+                    $lint_errors, $matches, PREG_SET_ORDER);
+            foreach ($matches as $match)
+                $this->fail($match[1], $match[2], "'\${$match[3]}'");
+        }
+    }
+}
+
+return 'UnitializedVars';
+?>