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'; +?>