diff --git a/include/class.signal.php b/include/class.signal.php
new file mode 100644
index 0000000000000000000000000000000000000000..86540054ed6d4d703b6f5e9175067298c51fa428
--- /dev/null
+++ b/include/class.signal.php
@@ -0,0 +1,93 @@
+<?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.
+     */
+    /*static*/ function connect($signal, $callable, $object=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");
+        $_subscribers[$signal][] = array($object, $callable);    
+    }
+
+    /**
+     * 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) = $sub;
+            if ($s && !is_a($object, $s))
+                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/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';
+?>