diff --git a/bootstrap.php b/bootstrap.php index c37e3f6f04d6080f31e1c4ed15f0e4313e6bb2fd..ae64d8c932578169b3da1d75964af75b18322516 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -111,6 +111,8 @@ class Bootstrap { define('FILTER_TABLE', $prefix.'filter'); define('FILTER_RULE_TABLE', $prefix.'filter_rule'); + define('PLUGIN_TABLE', $prefix.'plugin'); + define('API_KEY_TABLE',$prefix.'api_key'); define('TIMEZONE_TABLE',$prefix.'timezone'); } @@ -169,8 +171,7 @@ class Bootstrap { function loadCode() { #include required files - require(INCLUDE_DIR.'class.ostsession.php'); - require(INCLUDE_DIR.'class.usersession.php'); + require(INCLUDE_DIR.'class.auth.php'); require(INCLUDE_DIR.'class.pagenate.php'); //Pagenate helper! require(INCLUDE_DIR.'class.log.php'); require(INCLUDE_DIR.'class.crypto.php'); diff --git a/include/ajax.users.php b/include/ajax.users.php index 04b29bec611ee3b19d167645590950b4d3cd40ab..dd7615e41c16974f735de40f38a97793cbd9f1bc 100644 --- a/include/ajax.users.php +++ b/include/ajax.users.php @@ -118,5 +118,26 @@ class UsersAjaxAPI extends AjaxController { return $resp; } + function searchStaff() { + global $thisstaff; + + if (!$thisstaff) + Http::response(403, 'Login required for searching'); + elseif (!$thisstaff->isAdmin()) + Http::response(403, + 'Administrative privilege is required for searching'); + elseif (!isset($_REQUEST['q'])) + Http::response(400, 'Query argument is required'); + + $users = array(); + foreach (AuthenticationBackend::allRegistered() as $ab) { + if (!$ab->supportsSearch()) + continue; + + foreach ($ab->search($_REQUEST['q']) as $u) + $users[] = $u; + } + return $this->json_encode($users); + } } ?> diff --git a/include/class.auth.php b/include/class.auth.php new file mode 100644 index 0000000000000000000000000000000000000000..3609e2d8921a01ebe5ab38103e900380ce44f465 --- /dev/null +++ b/include/class.auth.php @@ -0,0 +1,233 @@ +<?php +require(INCLUDE_DIR.'class.ostsession.php'); +require(INCLUDE_DIR.'class.usersession.php'); + +class AuthenticatedUser { + // How the user was authenticated + var $backend; + + // Get basic information + function getId() {} + function getUsername() {} +} + +/** + * Authentication backend + * + * Authentication provides the basis of abstracting the link between the + * login page with a username and password and the staff member, + * administrator, or client using the system. + * + * The system works by allowing the AUTH_BACKENDS setting from + * ost-config.php to determine the list of authentication backends or + * providers and also specify the order they should be evaluated in. + * + * The authentication backend should define a authenticate() method which + * receives a username and optional password. If the authentication + * succeeds, an instance deriving from <User> should be returned. + */ +class AuthenticationBackend { + static private $registry = array(); + static $name; + static $id; + + /* static */ + static function register($class) { + if (is_string($class)) + $class = new $class(); + static::$registry[] = $class; + } + + static function allRegistered() { + return static::$registry; + } + + /* static */ + function process($username, $password=null, $backend=null, &$errors) { + global $ost; + + foreach (static::$registry as $bk) { + if ($backend && $bk->supportsAuthentication() && $bk::$id != $backend) + // User cannot be authenticated against this backend + continue; + $result = $bk->authenticate($username, $password); + if ($result instanceof AuthenticatedUser) { + //Log debug info. + $ost->logDebug('Staff login', + sprintf("%s logged in [%s], via %s", $result->getUserName(), + $_SERVER['REMOTE_ADDR'], get_class($bk))); //Debug. + + if ($result instanceof Staff) { + $sql='UPDATE '.STAFF_TABLE.' SET lastlogin=NOW() ' + .' WHERE staff_id='.db_input($result->getId()); + db_query($sql); + //Now set session crap and lets roll baby! + $_SESSION['_staff'] = array(); //clear. + $_SESSION['_staff']['userID'] = $username; + $result->refreshSession(); //set the hash. + + $_SESSION['TZ_OFFSET'] = $result->getTZoffset(); + $_SESSION['TZ_DST'] = $result->observeDaylight(); + + $_SESSION['_staff']['backend'] = $bk; + } + + //Regenerate session id. + $sid = session_id(); //Current id + session_regenerate_id(true); + // Destroy old session ID - needed for PHP version < 5.1.0 + // DELME: 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', $result); + + $result->cancelResetTokens(); + + return $result; + } + // TODO: Handle permission denied, for instance + elseif ($result instanceof AccessDenied) { + $errors['err'] = $result->reason; + break; + } + } + $info = array('username'=>$username, 'password'=>$password); + Signal::send('auth.login.failed', null, $info); + } + + /** + * Fetches the friendly name of the backend + */ + function getName() { + return static::$name; + } + + /** + * Indicates if the backed supports authentication. Useful if the + * backend is used for logging or lockout only + */ + function supportsAuthentication() { + return true; + } + + /** + * Indicates if the backend can be used to search for user information. + * Lookup is performed to find user information based on a unique + * identifier. + */ + function supportsLookup() { + return false; + } + + /** + * Indicates if the backend supports searching for usernames. This is + * distinct from information lookup in that lookup is intended to lookup + * information based on a unique identifier + */ + function supportsSearch() { + return false; + } + + /** + * Indicates if the backend supports changing a user's password. This + * would be done in two fashions. Either the currently-logged in user + * want to change its own password or a user requests to have their + * password reset. This requires an administrative privilege which this + * backend might not possess, so it's defined in supportsPasswordReset() + */ + function supportsPasswordChange() { + return false; + } + + function supportsPasswordReset() { + return false; + } +} + +class RemoteAuthenticationBackend { + var $create_unknown_user = false; +} + +/** + * This will be an exception in later versions of PHP + */ +class AccessDenied { + function AccessDenied() { + call_user_func_array(array($this, '__construct'), func_get_args()); + } + function __construct($reason) { + $this->reason = $reason; + } +} + +/** + * Simple authentication backend which will lock the login form after a + * configurable number of attempts + */ +class AuthLockoutBackend extends AuthenticationBackend { + + function authenticate($username, $password=null) { + global $cfg, $ost; + + if($_SESSION['_staff']['laststrike']) { + if((time()-$_SESSION['_staff']['laststrike'])<$cfg->getStaffLoginTimeout()) { + $_SESSION['_staff']['laststrike'] = time(); //reset timer. + return new AccessDenied('Max. failed login attempts reached'); + } else { //Timeout is over. + //Reset the counter for next round of attempts after the timeout. + $_SESSION['_staff']['laststrike']=null; + $_SESSION['_staff']['strikes']=0; + } + } + + $_SESSION['_staff']['strikes']+=1; + if($_SESSION['_staff']['strikes']>$cfg->getStaffMaxLogins()) { + $_SESSION['_staff']['laststrike']=time(); + $alert='Excessive login attempts by a staff member?'."\n". + 'Username: '.$username."\n" + .'IP: '.$_SERVER['REMOTE_ADDR']."\n" + .'TIME: '.date('M j, Y, g:i a T')."\n\n" + .'Attempts #'.$_SESSION['_staff']['strikes']."\n" + .'Timeout: '.($cfg->getStaffLoginTimeout()/60)." minutes \n\n"; + $ost->logWarning('Excessive login attempts ('.$username.')', $alert, + $cfg->alertONLoginError()); + return new AccessDenied('Forgot your login info? Contact Admin.'); + //Log every other failed login attempt as a warning. + } elseif($_SESSION['_staff']['strikes']%2==0) { + $alert='Username: '.$username."\n" + .'IP: '.$_SERVER['REMOTE_ADDR']."\n" + .'TIME: '.date('M j, Y, g:i a T')."\n\n" + .'Attempts #'.$_SESSION['_staff']['strikes']; + $ost->logWarning('Failed staff login attempt ('.$username.')', $alert, false); + } + } + + function supportsAuthentication() { + return false; + } +} +AuthenticationBackend::register(AuthLockoutBackend); + +class osTicketAuthentication extends AuthenticationBackend { + static $name = "Local Authenication"; + static $id = "local"; + + function authenticate($username, $password) { + if (($user = new StaffSession($username)) && $user->getId() && + $user->check_passwd($password)) { + + //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); + + return $user; + } + } +} +AuthenticationBackend::register(osTicketAuthentication); +?> diff --git a/include/class.config.php b/include/class.config.php index ca426bd2c4661f8f417f961cbebc884f3d977cfd..22c984e43be176facb73b977316214e5680b9427 100644 --- a/include/class.config.php +++ b/include/class.config.php @@ -14,8 +14,6 @@ vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ -require_once(INCLUDE_DIR.'class.email.php'); - class Config { var $config = array(); @@ -105,7 +103,9 @@ class Config { } function update($key, $value) { - if (!isset($this->config[$key])) + if (!$key) + return false; + elseif (!isset($this->config[$key])) return $this->create($key, $value); $setting = &$this->config[$key]; diff --git a/include/class.cron.php b/include/class.cron.php index 3aa0357c198ce61e25a22dae6bc9ea982954877e..999bf437dd60b281d00d8c9e2c6c55e1ef220dd4 100644 --- a/include/class.cron.php +++ b/include/class.cron.php @@ -3,7 +3,7 @@ class.cron.php Nothing special...just a central location for all cron calls. - + Peter Rotich <peter@osticket.com> Copyright (c) 2006-2013 osTicket http://www.osticket.com @@ -12,10 +12,12 @@ See LICENSE.TXT for details. TODO: The plan is to make cron jobs db based. - + vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ //TODO: Make it DB based! +require_once INCLUDE_DIR.'class.signal.php'; + class Cron { function MailFetcher() { @@ -27,7 +29,7 @@ class Cron { require_once(INCLUDE_DIR.'class.ticket.php'); require_once(INCLUDE_DIR.'class.lock.php'); Ticket::checkOverdue(); //Make stale tickets overdue - TicketLock::cleanup(); //Remove expired locks + TicketLock::cleanup(); //Remove expired locks } function PurgeLogs() { @@ -55,6 +57,8 @@ class Cron { self::PurgeLogs(); self::CleanOrphanedFiles(); self::PurgeDrafts(); + + Signal::send('cron'); } } ?> diff --git a/include/class.forms.php b/include/class.forms.php index 0654a2648c9d1cb2438e35c0357927dc7d2588b7..35121204b94e991dcb25764a49a2fd2377bbace7 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -82,6 +82,8 @@ class Form { if (!$this->_clean) { $this->_clean = array(); foreach ($this->getFields() as $key=>$field) { + if (!$field->hasData()) + continue; $this->_clean[$key] = $this->_clean[$field->get('name')] = $field->getClean(); } @@ -129,14 +131,14 @@ class FormField { static $types = array( 'Basic Fields' => array( - 'text' => array('Short Answer', TextboxField), - 'memo' => array('Long Answer', TextareaField), - 'thread' => array('Thread Entry', ThreadEntryField, false), - 'datetime' => array('Date and Time', DatetimeField), - 'phone' => array('Phone Number', PhoneField), - 'bool' => array('Checkbox', BooleanField), - 'choices' => array('Choices', ChoiceField), - 'break' => array('Section Break', SectionBreakField), + 'text' => array('Short Answer', 'TextboxField'), + 'memo' => array('Long Answer', 'TextareaField'), + 'thread' => array('Thread Entry', 'ThreadEntryField', false), + 'datetime' => array('Date and Time', 'DatetimeField'), + 'phone' => array('Phone Number', 'PhoneField'), + 'bool' => array('Checkbox', 'BooleanField'), + 'choices' => array('Choices', 'ChoiceField'), + 'break' => array('Section Break', 'SectionBreakField'), ), ); static $more_types = array(); @@ -441,7 +443,8 @@ class FormField { if (!static::$widget) throw new Exception('Widget not defined for this field'); if (!isset($this->_widget)) { - $this->_widget = new static::$widget($this); + $wc = $this->get('widget') ? $this->get('widget') : static::$widget; + $this->_widget = new $wc($this); $this->_widget->parseValue(); } return $this->_widget; @@ -500,6 +503,18 @@ class TextboxField extends FormField { } } +class PasswordField extends TextboxField { + static $widget = 'PasswordWidget'; + + function to_database($value) { + return Crypto::encrypt($value, SECRET_SALT, $this->getFormName()); + } + + function to_php($value) { + return Crypto::decrypt($value, SECRET_SALT, $this->getFormName()); + } +} + class TextareaField extends FormField { static $widget = 'TextareaWidget'; @@ -841,6 +856,8 @@ class Widget { } class TextboxWidget extends Widget { + static $input_type = 'text'; + function render() { $config = $this->field->getConfiguration(); if (isset($config['size'])) @@ -853,7 +870,8 @@ class TextboxWidget extends Widget { $autocomplete = 'autocomplete="'.($config['autocomplete']?'on':'off').'"'; ?> <span style="display:inline-block"> - <input type="text" id="<?php echo $this->name; ?>" + <input type="<?php echo static::$input_type; ?>" + id="<?php echo $this->name; ?>" <?php echo $size . " " . $maxlength; ?> <?php echo $classes.' '.$autocomplete; ?> name="<?php echo $this->name; ?>" @@ -863,6 +881,19 @@ class TextboxWidget extends Widget { } } +class PasswordWidget extends TextboxWidget { + static $input_type = 'password'; + + function parseValue() { + // Show empty box unless failed POST + if ($_SERVER['REQUEST_METHOD'] == 'POST' + && $this->field->getForm()->isValid()) + parent::parseValue(); + else + $this->value = ''; + } +} + class TextareaWidget extends Widget { function render() { $config = $this->field->getConfiguration(); diff --git a/include/class.nav.php b/include/class.nav.php index 5a391ff553967c1338828ab6796ea079a615a6dd..799c76a8b4dc0a78b74374c2b2aa673cf74c5132 100644 --- a/include/class.nav.php +++ b/include/class.nav.php @@ -213,6 +213,7 @@ class AdminNav extends StaffNav{ $subnav[]=array('desc'=>'Pages', 'href'=>'pages.php','title'=>'Pages','iconclass'=>'pages'); $subnav[]=array('desc'=>'Forms','href'=>'forms.php','iconclass'=>'forms'); $subnav[]=array('desc'=>'Lists','href'=>'lists.php','iconclass'=>'lists'); + $subnav[]=array('desc'=>'Plugins','href'=>'plugins.php','iconclass'=>'api'); break; case 'emails': $subnav[]=array('desc'=>'Emails','href'=>'emails.php', 'title'=>'Email Addresses', 'iconclass'=>'emailSettings'); diff --git a/include/class.osticket.php b/include/class.osticket.php index 875dc13bf8e2e6217a881db22448c0251a019cc5..0e76fe3e6f4f02c6c79b72081182e90d2c002c91 100644 --- a/include/class.osticket.php +++ b/include/class.osticket.php @@ -20,6 +20,7 @@ require_once(INCLUDE_DIR.'class.csrf.php'); //CSRF token class. require_once(INCLUDE_DIR.'class.migrater.php'); +require_once(INCLUDE_DIR.'class.plugin.php'); define('LOG_WARN',LOG_WARNING); @@ -46,6 +47,7 @@ class osTicket { var $session; var $csrf; var $company; + var $plugins; function osTicket() { @@ -59,6 +61,8 @@ class osTicket { $this->csrf = new CSRF('__CSRFToken__'); $this->company = new Company(); + + $this->plugins = new PluginManager(); } function isSystemOnline() { @@ -432,6 +436,9 @@ class osTicket { $_SESSION['TZ_OFFSET'] = $ost->getConfig()->getTZoffset(); $_SESSION['TZ_DST'] = $ost->getConfig()->observeDaylightSaving(); + // Bootstrap installed plugins + $ost->plugins->bootstrap(); + return $ost; } } diff --git a/include/class.plugin.php b/include/class.plugin.php new file mode 100644 index 0000000000000000000000000000000000000000..76ad2d24c5aecc6b16e72310b632573e5883695e --- /dev/null +++ b/include/class.plugin.php @@ -0,0 +1,324 @@ +<?php + +require_once(INCLUDE_DIR.'/class.config.php'); +class PluginConfig extends Config { + var $table = CONFIG_TABLE; + var $form; + + function __construct($name) { + // Use parent constructor to place configurable information into the + // central config table in a namespace of "plugin.<id>" + parent::Config("plugin.$name"); + } + + /* abstract */ + function getOptions() { + return array(); + } + + /** + * Retreive a Form instance for the configurable options offered in + * ::getOptions + */ + function getForm() { + if (!isset($this->form)) { + $this->form = new Form($this->getOptions()); + if ($_SERVER['REQUEST_METHOD'] != 'POST') + $this->form->data($this->getInfo()); + } + return $this->form; + } + + /** + * commit + * + * Used in the POST request of the configuration process. The + * ::getForm() method should be used to retrieve a configuration form + * for this plugin. That form should be submitted via a POST request, + * and this method should be called in that request. The data from the + * POST request will be interpreted and will adjust the configuration of + * this field + * + * Parameters: + * errors - (OUT array) receives validation errors of the parsed + * configuration form + * + * Returns: + * (bool) true if the configuration was updated, false if there were + * errors. If false, the errors were written into the received errors + * array. + */ + function commit(&$errors=array()) { + $f = $this->getForm(); + if ($f->isValid()) { + $config = $f->getClean(); + $this->pre_save($config, $errors); + } + $errors += $f->errors(); + if (count($errors) === 0) + return $this->updateAll($config); + return false; + } + + /** + * Pre-save hook to check configuration for errors (other than obvious + * validation errors) prior to saving + */ + function pre_save($config, &$errors) { + return; + } + + /** + * Remove all configuration for this plugin -- used when the plugin is + * uninstalled + */ + function purge() { + $sql = 'DELETE FROM '.$this->table + .' WHERE `namespace`='.db_input($this->getNamespace()); + return (db_query($sql) && db_affected_rows()); + } +} + +class PluginManager { + static private $plugin_info = array(); + + /** + * boostrap + * + * Used to bootstrap the plugin subsystem and initialize all the plugins + * currently enabled. + */ + function bootstrap() { + foreach ($this->allActive() as $p) + $p->bootstrap(); + } + + /** + * allActive + * + * Scans the plugin registry to find all installed and active plugins. + * Those plugins are included, instanciated, and cached in a list. + * + * Returns: + * Array<Plugin> a cached list of instanciated plugins for all installed + * and active plugins + */ + static function allInstalled() { + static $plugins = null; + if ($plugins !== null) + return $plugins; + + $plugins = array(); + $sql = 'SELECT * FROM '.PLUGIN_TABLE; + if (!($res = db_query($sql))) + return $plugins; + + $infos = static::allInfos(); + while ($ht = db_fetch_array($res)) { + // XXX: Only read active plugins here. allInfos() will + // read all plugins + if (isset($infos[$ht['install_path']])) { + $info = $infos[$ht['install_path']]; + if ($ht['isactive']) { + list($path, $class) = explode(':', $info['plugin']); + require_once(INCLUDE_DIR . '/' . $ht['install_path'] . '/' . $path); + $plugins[$ht['install_path']] = new $class($ht['id']); + } + else { + $plugins[$ht['install_path']] = $ht; + } + } + } + return $plugins; + } + + static function allActive() { + $plugins = array(); + foreach (static::allInstalled() as $p) + if ($p instanceof Plugin && $p->isActive()) + $plugins[] = $p; + return $plugins; + } + + /** + * allInfos + * + * Scans the plugin folders for installed plugins. For each one, the + * plugin.php file is included and the info array returned in added to + * the list returned. + * + * Returns: + * Information about all available plugins. The registry will have to be + * queried to determine if the plugin is installed + */ + static function allInfos() { + static $defaults = array( + 'include' => 'include/', + 'stream' => false, + ); + + if (static::$plugin_info) + return static::$plugin_info; + + foreach (glob(INCLUDE_DIR . 'plugins/*', GLOB_ONLYDIR) as $p) { + if (!is_file($p . '/plugin.php')) + // Invalid plugin -- must define "/plugin.php" + continue; + // plugin.php is require to return an array of informaiton about + // the plugin. + $info = array_merge($defaults, (include $p . '/plugin.php')); + $info['install_path'] = str_replace(INCLUDE_DIR, '', $p); + + // XXX: Ensure 'id' key isset + static::$plugin_info[$info['install_path']] = $info; + } + return static::$plugin_info; + } + + static function getInfoForPath($path) { + $infos = static::allInfos(); + if (isset($infos[$path])) + return $infos[$path]; + return null; + } + + function getInstance($path) { + static $instances = array(); + if (!isset($instances[$path]) + && ($ps = static::allInstalled()) + && ($ht = $ps[$path]) + && ($info = static::getInfoForPath($path))) { + // $ht may be the plugin instance + if ($ht instanceof Plugin) + return $ht; + // Usually this happens when the plugin is being enabled + list($path, $class) = explode(':', $info['plugin']); + require_once(INCLUDE_DIR . $info['install_path'] . '/' . $path); + $instances[$path] = new $class($ht['id']); + } + return $instances[$path]; + } + + /** + * install + * + * Used to install a plugin that is in-place on the filesystem, but not + * registered in the plugin registry -- the %plugin table. + */ + function install($path) { + if (!($info = $this->getInfoForPath($path))) + return false; + + $sql='INSERT INTO '.PLUGIN_TABLE.'SET installed=NOW() ' + .', install_path='.db_input($path) + .', name='.db_input($info['name']); + return (db_query($sql) && db_affected_rows()); + } +} + +/** + * Class: Plugin (abstract) + * + * Base class for plugins. Plugins should inherit from this class and define + * the useful pieces of the + */ +class Plugin { + /** + * Configuration manager for the plugin. Should be the name of a class + * that inherits from PluginConfig. This is abstract and must be defined + * by the plugin subclass. + */ + var $config_class = null; + var $id; + var $info; + + function Plugin($id) { + $this->id = $id; + $this->load(); + } + + function load() { + $sql = 'SELECT * FROM '.PLUGIN_TABLE.' WHERE + `id`='.db_input($this->id); + if (($res = db_query($sql)) && ($ht=db_fetch_array($res))) + $this->ht = $ht; + $this->info = PluginManager::getInfoForPath($this->ht['install_path']); + } + + function getId() { return $this->id; } + function getName() { return $this->info['name']; } + function isActive() { return $this->ht['isactive']; } + function isPhar() { return $this->ht['isphar']; } + function getInstallDate() { return $this->ht['installed']; } + + function getIncludePath() { + return realpath(INCLUDE_DIR . $this->info['install_path'] . '/' + . $this->info['include_path']) . '/'; + } + + /** + * uninstall + * + * Removes the plugin from the plugin registry. The files remain on the + * filesystem which would allow the plugin to be reinstalled. The + * configuration for the plugin is also removed. If the plugin is + * reinstalled, it will have to be reconfigured. + */ + function uninstall() { + $sql = 'DELETE FROM '.PLUGIN_TABLE + .' WHERE id='.db_input($this->getId()); + if (db_query($sql) && db_affected_rows()) + return $this->getConfig()->purge(); + return false; + } + + function enable() { + $sql = 'UPDATE '.PLUGIN_TABLE + .' SET isactive=1 WHERE id='.db_input($this->getId()); + return (db_query($sql) && db_affected_rows()); + } + + function disable() { + $sql = 'UPDATE '.PLUGIN_TABLE + .' SET isactive=0 WHERE id='.db_input($this->getId()); + return (db_query($sql) && db_affected_rows()); + } + + /** + * upgrade + * + * Upgrade the plugin. This is used to migrate the database pieces of + * the plugin using the database migration stream packaged with the + * plugin. + */ + function upgrade() { + } + + function getConfig() { + static $config = null; + if ($config === null) + $config = new $this->config_class($this->getId()); + + return $config; + } + + function source($what) { + $what = str_replace('\\', '/', $what); + if ($what && $what[0] != '/') + $what = $this->getIncludePath() . $what; + include_once $what; + } + + static function lookup($id) { //Assuming local ID is the only lookup used! + $path = false; + if ($id && is_numeric($id)) { + $sql = 'SELECT install_path FROM '.PLUGIN_TABLE + .' WHERE id='.db_input($id); + $path = db_result(db_query($sql)); + } + if ($path) + return PluginManager::getInstance($path); + } +} + +?> diff --git a/include/class.staff.php b/include/class.staff.php index 93708bc0cccfbd71120f321f22e722f7af98834e..f2e3d6b384fbc5b846155d3108792aff24c6021e 100644 --- a/include/class.staff.php +++ b/include/class.staff.php @@ -20,8 +20,9 @@ include_once(INCLUDE_DIR.'class.team.php'); include_once(INCLUDE_DIR.'class.group.php'); include_once(INCLUDE_DIR.'class.passwd.php'); include_once(INCLUDE_DIR.'class.user.php'); +include_once(INCLUDE_DIR.'class.auth.php'); -class Staff { +class Staff extends AuthenticatedUser { var $ht; var $id; @@ -762,13 +763,17 @@ class Staff { $errors['mobile']='Valid number required'; if($vars['passwd1'] || $vars['passwd2'] || !$id) { - if(!$vars['passwd1'] && !$id) { + if($vars['passwd1'] && strcmp($vars['passwd1'], $vars['passwd2'])) { + $errors['passwd2']='Password(s) do not match'; + } + elseif ($vars['backend'] != 'local') { + // Password can be omitted + } + elseif(!$vars['passwd1'] && !$id) { $errors['passwd1']='Temp. password required'; $errors['temppasswd']='Required'; } elseif($vars['passwd1'] && strlen($vars['passwd1'])<6) { $errors['passwd1']='Must be at least 6 characters'; - } elseif($vars['passwd1'] && strcmp($vars['passwd1'], $vars['passwd2'])) { - $errors['passwd2']='Password(s) do not match'; } } @@ -798,6 +803,7 @@ class Staff { .' ,firstname='.db_input($vars['firstname']) .' ,lastname='.db_input($vars['lastname']) .' ,email='.db_input($vars['email']) + .' ,backend='.db_input($vars['backend']) .' ,phone="'.db_input(Format::phone($vars['phone']),false).'"' .' ,phone_ext='.db_input($vars['phone_ext']) .' ,mobile="'.db_input(Format::phone($vars['mobile']),false).'"' diff --git a/include/staff/plugin.inc.php b/include/staff/plugin.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..f57b12b93a7f86d969231f20bb88ae7819990311 --- /dev/null +++ b/include/staff/plugin.inc.php @@ -0,0 +1,41 @@ +<?php + +$info=array(); +if($plugin && $_REQUEST['a']!='add') { + $form = $plugin->getConfig()->getForm(); + if ($_POST) + $form->isValid(); + $title = 'Update Plugin'; + $action = 'update'; + $submit_text='Save Changes'; + $info = $plugin->ht; + $newcount=2; +} else { + $title = 'Install plugin'; + $action = 'add'; + $submit_text='Install'; + $newcount=4; +} +$info=Format::htmlchars(($errors && $_POST)?$_POST:$info); +?> + +<form action="?id=<?php echo urlencode($_REQUEST['id']); ?>" method="post" id="save"> + <?php csrf_token(); ?> + <input type="hidden" name="do" value="<?php echo $action; ?>"> + <input type="hidden" name="id" value="<?php echo $info['id']; ?>"> + <h2>Manage Plugin + <br/><small><?php echo $plugin->getName(); ?></small></h2> + + <h3>Configuration</h3> + <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <tbody> +<?php +$form->render(); +?> + </tbody></table> +<p class="centered"> + <input type="submit" name="submit" value="<?php echo $submit_text; ?>"> + <input type="reset" name="reset" value="Reset"> + <input type="button" name="cancel" value="Cancel" onclick='window.location.href="?"'> +</p> +</form> diff --git a/include/staff/plugins.inc.php b/include/staff/plugins.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..eab4a796ee6a3240cd30d35fac1e20f2071b17f0 --- /dev/null +++ b/include/staff/plugins.inc.php @@ -0,0 +1,104 @@ +<div style="width:700;padding-top:5px; float:left;"> + <h2>Currently Installed Plugins</h2> +</div> +<div style="float:right;text-align:right;padding-top:5px;padding-right:5px;"> + <b><a href="plugins.php?a=add" class="Icon form-add">Add New Plugin</a></b></div> +<div class="clear"></div> + +<?php +$page = ($_GET['p'] && is_numeric($_GET['p'])) ? $_GET['p'] : 1; +$count = count($ost->plugins->allInstalled()); +$pageNav = new Pagenate($count, $page, PAGE_LIMIT); +$pageNav->setURL('forms.php'); +$showing=$pageNav->showing().' forms'; +?> + +<form action="plugins.php" method="POST" name="forms"> +<?php csrf_token(); ?> +<input type="hidden" name="do" value="mass_process" > +<input type="hidden" id="action" name="a" value="" > +<table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> + <thead> + <tr> + <th width="7"> </th> + <th>Plugin Name</th> + <th>Status</td> + <th>Date Installed</th> + </tr> + </thead> + <tbody> +<?php +foreach ($ost->plugins->allInstalled() as $p) { + if ($p instanceof Plugin) { ?> + <tr> + <td><input type="checkbox" class="ckb" name="ids[]" value="<?php echo $p->getId(); ?>" + <?php echo $sel?'checked="checked"':''; ?>></td> + <td><a href="plugins.php?id=<?php echo $p->getId(); ?>" + ><?php echo $p->getName(); ?></a></td> + <td>Enabled</td> + <td><?php echo Format::db_datetime($p->getInstallDate()); ?></td> + </tr> + <?php } else { + $p = $ost->plugins->getInfoForPath($p['install_path']); ?> + <tr> + <td><input type="checkbox" class="ckb" name="ids[]" value="<?php echo $p['install_path']; ?>" + <?php echo $sel?'checked="checked"':''; ?>></td> + <td><?php echo $p['name']; ?></td> + <td><strong>Disabled</strong></td> + <td></td> + </tr> + <?php } ?> +<?php } ?> + </tbody> + <tfoot> + <tr> + <td colspan="4"> + <?php if($count){ ?> + Select: + <a id="selectAll" href="#ckb">All</a> + <a id="selectNone" href="#ckb">None</a> + <a id="selectToggle" href="#ckb">Toggle</a> + <?php }else{ + echo 'No extra forms defined yet — add one!'; + } ?> + </td> + </tr> + </tfoot> +</table> +<?php +if ($count) //Show options.. + echo '<div> Page:'.$pageNav->getPageLinks().' </div>'; +?> +<p class="centered" id="actions"> + <input class="button" type="submit" name="delete" value="Delete"> + <input class="button" type="submit" name="enable" value="Enable"> + <input class="button" type="submit" name="disable" value="Disable"> +</p> +</form> + +<div style="display:none;" class="dialog" id="confirm-action"> + <h3>Please Confirm</h3> + <a class="close" href="">×</a> + <hr/> + <p class="confirm-action" style="display:none;" id="delete-confirm"> + <font color="red"><strong>Are you sure you want to DELETE selected plugins?</strong></font> + <br><br>Deleted forms CANNOT be recovered. + </p> + <p class="confirm-action" style="display:none;" id="enable-confirm"> + <font color="green"><strong>Are you ready to enable selected plugins?</strong></font> + </p> + <p class="confirm-action" style="display:none;" id="disable-confirm"> + <font color="red"><strong>Are you sure you want to disable selected plugins?</strong></font> + </p> + <div>Please confirm to continue.</div> + <hr style="margin-top:1em"/> + <p class="full-width"> + <span class="buttons" style="float:left"> + <input type="button" value="No, Cancel" class="close"> + </span> + <span class="buttons" style="float:right"> + <input type="button" value="Yes, Do it!" class="confirm"> + </span> + </p> + <div class="clear"></div> +</div> diff --git a/include/staff/staff.inc.php b/include/staff/staff.inc.php index 4567624842d7aaee268dfe7e034e4173b82c2afd..667b462312b603a205c912584dafb35fe15ac06c 100644 --- a/include/staff/staff.inc.php +++ b/include/staff/staff.inc.php @@ -17,12 +17,12 @@ if($staff && $_REQUEST['a']!='add'){ $title='Add New Staff'; $action='create'; $submit_text='Add Staff'; - $passwd_text='Temp. password required <span class="error"> *</span>'; + $passwd_text='Temporary password required only for "Local" authenication'; //Some defaults for new staff. $info['change_passwd']=1; $info['isactive']=1; $info['isvisible']=1; - $info['isadmin']=0; + $info['isadmin']=0; $qstr.='&a=add'; } $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); @@ -48,7 +48,8 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); Username: </td> <td> - <input type="text" size="30" name="username" value="<?php echo $info['username']; ?>"> + <input type="text" size="30" class="staff-username typeahead" + name="username" value="<?php echo $info['username']; ?>"> <span class="error">* <?php echo $errors['username']; ?></span> </td> </tr> @@ -58,7 +59,8 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); First Name: </td> <td> - <input type="text" size="30" name="firstname" value="<?php echo $info['firstname']; ?>"> + <input type="text" size="30" name="firstname" class="auto first" + value="<?php echo $info['firstname']; ?>"> <span class="error">* <?php echo $errors['firstname']; ?></span> </td> </tr> @@ -67,7 +69,8 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); Last Name: </td> <td> - <input type="text" size="30" name="lastname" value="<?php echo $info['lastname']; ?>"> + <input type="text" size="30" name="lastname" class="auto last" + value="<?php echo $info['lastname']; ?>"> <span class="error">* <?php echo $errors['lastname']; ?></span> </td> </tr> @@ -76,7 +79,8 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); Email Address: </td> <td> - <input type="text" size="30" name="email" value="<?php echo $info['email']; ?>"> + <input type="text" size="30" name="email" class="auto email" + value="<?php echo $info['email']; ?>"> <span class="error">* <?php echo $errors['email']; ?></span> </td> </tr> @@ -85,7 +89,8 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); Phone Number: </td> <td> - <input type="text" size="18" name="phone" value="<?php echo $info['phone']; ?>"> + <input type="text" size="18" name="phone" class="auto phone" + value="<?php echo $info['phone']; ?>"> <span class="error"> <?php echo $errors['phone']; ?></span> Ext <input type="text" size="5" name="phone_ext" value="<?php echo $info['phone_ext']; ?>"> <span class="error"> <?php echo $errors['phone_ext']; ?></span> @@ -96,15 +101,39 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); Mobile Number: </td> <td> - <input type="text" size="18" name="mobile" value="<?php echo $info['mobile']; ?>"> + <input type="text" size="18" name="mobile" class="auto mobile" + value="<?php echo $info['mobile']; ?>"> <span class="error"> <?php echo $errors['mobile']; ?></span> </td> </tr> <tr> <th colspan="2"> - <em><strong>Account Password</strong>: <?php echo $passwd_text; ?> <span class="error"> <?php echo $errors['temppasswd']; ?></span></em> + <em><strong>Authentication</strong>: <?php echo $passwd_text; ?> <span class="error"> <?php echo $errors['temppasswd']; ?></span></em> </th> </tr> + <tr> + <td>Authentication Backend</td> + <td> + <select name="backend" onchange="javascript: + if (this.value != '' && this.value != 'local') + $('#password-fields').hide(); + else + $('#password-fields').show(); + "> + <option value="">— Use any available backend —</option> + <?php foreach (AuthenticationBackend::allRegistered() as $ab) { + if (!$ab->supportsAuthentication()) continue; ?> + <option value="<?php echo $ab::$id; ?>" <?php + if ($info['backend'] == $ab::$id) + echo 'selected="selected"'; ?>><?php + echo $ab::$name; ?></option> + <?php } ?> + </select> + </td> + </tr> + </tbody> + <tbody id="password-fields" style="<?php if ($info['backend'] && $info['backend'] != 'local') + echo 'display:none;'; ?>"> <tr> <td width="180"> Password: @@ -133,6 +162,8 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); <strong>Force</strong> password change on next login. </td> </tr> + </tbody> + <tbody> <tr> <th colspan="2"> <em><strong>Staff's Signature</strong>: Optional signature used on outgoing emails. <span class="error"> <?php echo $errors['signature']; ?></span></em> diff --git a/include/staff/templates/dynamic-form.tmpl.php b/include/staff/templates/dynamic-form.tmpl.php index f77fa928a74148164d24a6af8eb3527aa6593682..cfe25a45ad544779d916179e55f639edb6073a49 100644 --- a/include/staff/templates/dynamic-form.tmpl.php +++ b/include/staff/templates/dynamic-form.tmpl.php @@ -12,7 +12,7 @@ <?php } else { ?> - <td class="multi-line <?php if ($field->get('required')) echo 'required'; ?>"> + <td class="multi-line <?php if ($field->get('required')) echo 'required'; ?>" style="min-width:120px;"> <?php echo Format::htmlchars($field->get('label')); ?>:</td> <td><?php } diff --git a/scp/ajax.php b/scp/ajax.php index 106f3366a723a6e25a815796209c1dc5fed07f71..1dcbe44aad94e8ca9a26794266f5cb4243d06975 100644 --- a/scp/ajax.php +++ b/scp/ajax.php @@ -64,7 +64,8 @@ $dispatcher = patterns('', url_get('^/lookup/form$', 'getLookupForm'), url_post('^/lookup/form$', 'addUser'), url_get('^/select$', 'selectUser'), - url_get('^/select/(?P<id>\d+)$', 'selectUser') + url_get('^/select/(?P<id>\d+)$', 'selectUser'), + url_get('^/staff$', 'searchStaff') )), url('^/tickets/', patterns('ajax.tickets.php:TicketsAjaxAPI', url_get('^(?P<tid>\d+)/change-user$', 'changeUserForm'), @@ -93,6 +94,8 @@ $dispatcher = patterns('', )) ); +Signal::send('ajax.scp', $dispatcher); + # Call the respective function print $dispatcher->resolve($ost->get_path_info()); ?> diff --git a/scp/js/scp.js b/scp/js/scp.js index b61e62057084e164bf1aac9e146f13f292ba9b3b..f01da5f197969a341596cabab1b830a07778e234 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -345,6 +345,26 @@ $(document).ready(function(){ }, property: "email" }); + $('.staff-username.typeahead').typeahead({ + source: function (typeahead, query) { + if(query.length > 2) { + $.ajax({ + url: "ajax.php/users/staff?q="+query, + dataType: 'json', + success: function (data) { + typeahead.process(data); + } + }); + } + }, + onselect: function (obj) { + var fObj=$('.staff-username.typeahead').closest('form'); + $.each(['first','last','email','phone','mobile'], function(i,k) { + if (obj[k]) $('.auto.'+k, fObj).val(obj[k]); + }); + }, + property: "username" + }); //Overlay $('#overlay').css({ diff --git a/scp/login.php b/scp/login.php index 2f3cf2236e9f4996bb10b94764fb6d0a14d99d22..7c13cf68b495fd76b12e3a33f744fc85c16d0a5c 100644 --- a/scp/login.php +++ b/scp/login.php @@ -23,8 +23,15 @@ $dest = $_SESSION['_staff']['auth']['dest']; $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['userid'], $_POST['passwd'], $errors))){ + // Lookup support backends for this staff + $username = trim($_POST['userid']); + $sql = 'SELECT backend FROM '.STAFF_TABLE + .' WHERE username='.db_input($username) + .' OR email='.db_input($username); + $backend = db_result(db_query($sql)); + + if ($user = AuthenticationBackend::process($username, + $_POST['passwd'], $backend, $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/plugins.php b/scp/plugins.php new file mode 100644 index 0000000000000000000000000000000000000000..7fdcb12aec33022ea9641895c59b3e59bbc1e5cd --- /dev/null +++ b/scp/plugins.php @@ -0,0 +1,47 @@ +<?php +require('admin.inc.php'); +require_once(INCLUDE_DIR."/class.plugin.php"); + +if($_REQUEST['id'] && !($plugin=Plugin::lookup($_REQUEST['id']))) + $errors['err']='Unknown or invalid plugin ID.'; + +if($_POST) { + switch(strtolower($_POST['do'])) { + case 'update': + if ($plugin) { + $plugin->getConfig()->commit($errors); + } + break; + case 'mass_process': + if(!$_POST['ids'] || !is_array($_POST['ids']) || !count($_POST['ids'])) { + $errors['err'] = 'You must select at least one plugin'; + } else { + $count = count($_POST['ids']); + switch(strtolower($_POST['a'])) { + case 'enable': + foreach ($_POST['ids'] as $path) { + if ($p = $ost->plugins->getInstance($path)) { + $p->enable(); + } + } + break; + case 'disable': + foreach ($_POST['ids'] as $id) { + if ($p = Plugin::lookup($id)) { + $p->disable(); + } + } + } + } + } +} + +$page = 'plugins.inc.php'; +if ($plugin) + $page = 'plugin.inc.php'; + +$nav->setTabActive('manage'); +require(STAFFINC_DIR.'header.inc.php'); +require(STAFFINC_DIR.$page); +include(STAFFINC_DIR.'footer.inc.php'); +?> diff --git a/setup/cli/package.php b/setup/cli/package.php index 15080fd8ab11eda31d6d09f06e0274f48de99a3b..e0b02cdea6725d3a8bd36387ba7e156c0765c32d 100755 --- a/setup/cli/package.php +++ b/setup/cli/package.php @@ -115,7 +115,7 @@ mkdir("$stage_path/scripts/"); package("setup/scripts/*", "scripts/", -1, "*stage"); # Load the heart of the system -package("include/{,.}*", "upload/include", -1, array('*ost-config.php', '*.sw[a-z]')); +package("include/{,.}*", "upload/include", -1, array('*ost-config.php', '*.sw[a-z]','plugins/*')); # Include the installer package("setup/*.{php,txt,html}", "upload/setup", -1, array("*scripts","*test","*stage")); diff --git a/setup/inc/streams/core/install-mysql.sql b/setup/inc/streams/core/install-mysql.sql index 8ab5676efd090558868eef5065c14b19882af0ed..94cfbd5e7e0c1f74d89aa85a24482b67f5848f5f 100644 --- a/setup/inc/streams/core/install-mysql.sql +++ b/setup/inc/streams/core/install-mysql.sql @@ -435,6 +435,7 @@ CREATE TABLE `%TABLE_PREFIX%staff` ( `firstname` varchar(32) default NULL, `lastname` varchar(32) default NULL, `passwd` varchar(128) default NULL, + `backend` varchar(32) default NULL, `email` varchar(128) default NULL, `phone` varchar(24) NOT NULL default '', `phone_ext` varchar(6) default NULL, @@ -676,6 +677,18 @@ CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%page` ( UNIQUE KEY `name` (`name`) ) DEFAULT CHARSET=utf8; +-- Plugins +DROP TABLE IF EXISTS `%TABLE_PREFIX%plugin`; +CREATE TABLE `%TABLE_PREFIX%plugin` ( + `id` int(11) unsigned not null auto_increment, + `name` varchar(30) not null, + `install_path` varchar(60) not null, + `isphar` tinyint(1) not null default 0, + `isactive` tinyint(1) not null default 0, + `installed` datetime not null, + primary key (`id`) +) DEFAULT CHARSET=utf8; + DROP TABLE IF EXISTS `%TABLE_PREFIX%user`; CREATE TABLE `%TABLE_PREFIX%user` ( `id` int(10) unsigned NOT NULL auto_increment,