diff --git a/Core/Frameworks/Baikal/Core/IMipSMTPPlugin.php b/Core/Frameworks/Baikal/Core/IMipSMTPPlugin.php new file mode 100644 index 00000000..3dda372d --- /dev/null +++ b/Core/Frameworks/Baikal/Core/IMipSMTPPlugin.php @@ -0,0 +1,197 @@ + + * @license http://sabre.io/license/ Modified BSD License + */ +class IMipSMTPPlugin extends \Sabre\DAV\ServerPlugin +{ + /** + * Email address used in From: header. + * + * @var string + */ + protected $senderEmail; + + /** + * SMTP connection made by PEAR + * + */ + protected $smtp; + + /** + * ITipMessage. + * + * @var ITip\Message + */ + protected $itipMessage; + + /** + * Creates the email handler. + * + * @param string $senderEmail. The 'senderEmail' is the email that shows up + * in the 'From:' address. This should + * generally be some kind of no-reply email + * address you own. + */ + public function __construct($senderEmail, $smtp_host = "", $smtp_port = "", $smtp_username = "", $smtp_password = "") + { + require_once "Mail.php"; + $this->senderEmail = $senderEmail; + $this->smtp = \Mail::factory('smtp', array ('host' => $smtp_host, 'port' => $smtp_port, 'auth' => true, 'username' => $smtp_username, 'password' => $smtp_password)); + } + + /* + * This initializes the plugin. + * + * This function is called by Sabre\DAV\Server, after + * addPlugin is called. + * + * This method should set up the required event subscriptions. + * + * @param DAV\Server $server + * @return void + */ + public function initialize(DAV\Server $server) + { + $server->on('schedule', [$this, 'schedule'], 120); + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using \Sabre\DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'imip-smtp'; + } + + /** + * Event handler for the 'schedule' event. + */ + public function schedule(ITip\Message $iTipMessage) + { + // Not sending any emails if the system considers the update + // insignificant. + if (!$iTipMessage->significantChange) { + if (!$iTipMessage->scheduleStatus) { + $iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email'; + } + + return; + } + + $summary = $iTipMessage->message->VEVENT->SUMMARY; + + if ('mailto' !== parse_url($iTipMessage->sender, PHP_URL_SCHEME)) { + return; + } + + if ('mailto' !== parse_url($iTipMessage->recipient, PHP_URL_SCHEME)) { + return; + } + + $sender = substr($iTipMessage->sender, 7); + $recipient = substr($iTipMessage->recipient, 7); + + if ($iTipMessage->senderName) { + $sender = $iTipMessage->senderName.' <'.$sender.'>'; + } + if ($iTipMessage->recipientName && $iTipMessage->recipientName != $recipient) { + $recipient = $iTipMessage->recipientName.' <'.$recipient.'>'; + } + + $subject = 'SabreDAV iTIP message'; + switch (strtoupper($iTipMessage->method)) { + case 'REPLY': + $subject = 'Re: '.$summary; + break; + case 'REQUEST': + $subject = 'Invitation: '.$summary; + break; + case 'CANCEL': + $subject = 'Cancelled: '.$summary; + break; + } + + $headers = array( + 'Reply-To' => $sender, + 'From' => $iTipMessage->senderName.' <'.$this->senderEmail.'>', + 'To' => $recipient, + 'Subject' => $subject, + 'MIME-Version' => '1.0', + 'Content-Type' => 'text/calendar; charset=UTF-8; method='.$iTipMessage->method, + ); + if (DAV\Server::$exposeVersion) { + $headers += ['X-Sabre-Version' => DAV\Version::VERSION]; + } + $this->mail( + $recipient, + $headers, + $iTipMessage->message->serialize() + ); + $iTipMessage->scheduleStatus = '1.1; Scheduling message is sent via iMip'; + } + + // @codeCoverageIgnoreStart + // This is deemed untestable in a reasonable manner + + /** + * This function is responsible for sending the actual email. + * + * @param string $to Recipient email address + * @param string $body iCalendar body + * @param array $headers List of headers + */ + protected function mail($to, array $headers, $body) + { + $mail = $this->smtp->send($to, $headers, $body); + if (\PEAR::isError($mail)) + error_log($mail->getMessage()); + else + error_log("Email successfully sent!"); + } + + // @codeCoverageIgnoreEnd + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'Email delivery (rfc6047) for CalDAV scheduling using Pear SMTP', + 'link' => 'http://sabre.io/dav/scheduling/', + ]; + } +} diff --git a/Core/Frameworks/Baikal/Core/LDAP.php b/Core/Frameworks/Baikal/Core/LDAP.php new file mode 100644 index 00000000..d926b043 --- /dev/null +++ b/Core/Frameworks/Baikal/Core/LDAP.php @@ -0,0 +1,320 @@ + + * @author El-Virus + * @license http://sabre.io/license/ Modified BSD License + */ +class LDAP extends \Sabre\DAV\Auth\Backend\AbstractBasic +{ + /** + * Reference to PDO connection. + * + * @var PDO + */ + protected $pdo; + + /** + * PDO table name we'll be using. + * + * @var string + */ + protected $table_name; + + /** + * LDAP mode. + * Defines if LDAP authentication should match + * by DN, Attribute, or Filter. + * + * @var string + */ + protected $ldap_mode; + + /** + * LDAP server uri. + * e.g. ldaps://ldap.example.org + * + * @var string + */ + protected $ldap_uri; + + /** + * LDAP bind dn. + * Defines the bind dn that Baikal is going to use + * when looking for an attribute or filtering. + * + * @var string + */ + protected $ldap_bind_dn; + + /** + * LDAP bind password. + * Defines the password used by Baikal for binding. + * + * @var string + */ + protected $ldap_bind_password; + + /** + * LDAP dn pattern for binding. + * + * %u - gets replaced by full username + * %U - gets replaced by user part when the + * username is an email address + * %d - gets replaced by domain part when the + * username is an email address + * %1-9 - gets replaced by parts of the the domain + * split by '.' in reverse order + * mail.example.org: %1 = org, %2 = example, %3 = mail + * + * @var string + */ + protected $ldap_dn; + + /** + * LDAP attribute to use for name. + * + * @var string + */ + protected $ldap_cn; + + /** + * LDAP attribute used for mail. + * + * @var string + */ + protected $ldap_mail; + + /** + * LDAP base path where to search for attributes + * and apply filters. + * + * @var string + */ + protected $ldap_search_base; + + /** + * LDAP attribute to search for. + * + * @var string + */ + protected $ldap_search_attribute; + + /** + * LDAP filter to apply. + * + * @var string + */ + protected $ldap_search_filter; + + /** + * LDAP group to check if a user is member of. + * + * @var string + */ + protected $ldap_group; + + /** + * Replaces patterns for their assigned value. + * + * @param string &$base + * @param string $username + * + */ + + protected function patternReplace(&$base, $username) + { + $user_split = explode('@', $username, 2); + $ldap_user = $user_split[0]; + $ldap_domain = ''; + if (count($user_split) > 1) + $ldap_domain = $user_split[1]; + $domain_split = array_reverse(explode('.', $ldap_domain)); + + $base = str_replace('%u', $username, $base); + $base = str_replace('%U', $ldap_user, $base); + $base = str_replace('%d', $ldap_domain, $base); + for($i = 1; $i <= count($domain_split) and $i <= 9; $i++) + $base = str_replace('%' . $i, $domain_split[$i - 1], $base); + } + + /** + * Checks if a user can bind with a password. + * If an error is produced, it will be logged. + * + * @param \LDAP\Connection &$conn + * @param string $dn + * @param string $password + * + * @return bool + */ + + protected function doesBind(&$conn, $dn, $password) + { + try { + $bind = ldap_bind($conn, $dn, $password); + if ($bind) + return true; + } catch (\ErrorException $e) { + error_log($e->getMessage()); + error_log(ldap_error($conn)); + } + return false; + } + + /** + * Creates the backend object. + * + * @param string $ldap_uri + * @param string $ldap_dn + * @param string $ldap_cn + * @param string $ldap_mail + * + */ + public function __construct(\PDO $pdo, $table_name = 'users', $ldap_mode = 'DN', $ldap_uri = 'ldap://127.0.0.1', $ldap_bind_dn = 'cn=baikal,ou=apps,dc=example,dc=com', $ldap_bind_password = '', $ldap_dn = 'mail=%u', $ldap_cn = 'cn', $ldap_mail = 'mail', $ldap_search_base = 'ou=users,dc=example,dc=com', $ldap_search_attribute = 'uid=%U', $ldap_search_filter = '(objectClass=*)', $ldap_group = 'cn=baikal,ou=groups,dc=example,dc=com') + { + $this->pdo = $pdo; + $this->table_name = $table_name; + $this->ldap_mode = $ldap_mode; + $this->ldap_uri = $ldap_uri; + $this->ldap_bind_dn = $ldap_bind_dn; + $this->ldap_bind_password = $ldap_bind_password; + $this->ldap_dn = $ldap_dn; + $this->ldap_cn = $ldap_cn; + $this->ldap_mail = $ldap_mail; + $this->ldap_search_base = $ldap_search_base; + $this->ldap_search_attribute = $ldap_search_attribute; + $this->ldap_search_filter = $ldap_search_filter; + $this->ldap_group = $ldap_group; + } + + /** + * Connects to an LDAP server and tries to authenticate. + * + * @param string $username + * @param string $password + * + * @return bool + */ + protected function ldapOpen($username, $password) + { + $conn = ldap_connect($this->ldap_uri); + if(!$conn) + return false; + if(!ldap_set_option($conn, LDAP_OPT_PROTOCOL_VERSION, 3)) + return false; + + $success = false; + + if ($this->ldap_mode == 'DN') { + $this->patternReplace($dn, $username); + + $success = $this->doesBind($conn, $dn, $password); + } elseif ($this->ldap_mode == 'Attribute' || $this->ldap_mode == 'Group') { + try { + if (!$this->doesBind($conn, $this->ldap_bind_dn, $this->ldap_bind_password)) { + return false; + } + + $attribute = $this->ldap_search_attribute; + $this->patternReplace($attribute, $username); + + $result = ldap_get_entries($conn, ldap_search($conn, $this->ldap_search_base, '('.$attribute.')', [explode('=', $attribute, 2)[0]], 0, 1, 0, LDAP_DEREF_ALWAYS, []))[0]; + + $dn = $result["dn"]; + + if ($this->ldap_mode == 'Group') { + $inGroup = FALSE; + $members = ldap_get_entries($conn, ldap_read($conn, $this->ldap_group, '(objectClass=*)', ['member', 'uniqueMember'], 0, 0, 0, LDAP_DEREF_NEVER, []))[0]; + if (isset($members["member"])) { + foreach ($members["member"] as $member) { + if ($member == $result["dn"]) { + $inGroup = TRUE; + break; + } + } + } + if (isset($members["uniqueMember"])) { + foreach ($members["uniqueMember"] as $member) { + if ($member == $result["dn"]) { + $inGroup = TRUE; + break; + } + } + } + if (!$inGroup) + return false; + } + + $success = $this->doesBind($conn, $dn, $password); + } catch (\ErrorException $e) { + error_log($e->getMessage()); + error_log(ldap_error($conn)); + } + } elseif ($this->ldap_mode == 'Filter') { + try { + if (!$this->doesBind($conn, $this->ldap_bind_dn, $this->ldap_bind_password)) { + return false; + } + + $filter = $this->ldap_search_filter; + $this->patternReplace($filter, $username); + + $result = ldap_get_entries($conn, ldap_search($conn, $this->ldap_search_base, $filter, [], 0, 1, 0, LDAP_DEREF_ALWAYS, []))[0]; + + $dn = $result["dn"]; + $success = $this->doesBind($conn, $dn, $password); + } catch (\ErrorException $e) { + error_log($e->getMessage()); + error_log(ldap_error($conn)); + } + } else { + error_log('Unknown LDAP authentication mode'); + } + + if($success){ + $stmt = $this->pdo->prepare('SELECT username, digesta1 FROM ' . $this->table_name . ' WHERE username = ?'); + $stmt->execute([$username]); + $result = $stmt->fetchAll(); + + if (empty($result)) { + $search_results = ldap_read($conn, $dn, '(objectclass=*)', array($this->ldap_cn, $this->ldap_mail)); + $entry = ldap_get_entries($conn, $search_results); + $user_displayname = $username; + $user_email = 'unset-email'; + if (!empty($entry[0][$this->ldap_cn])) + $user_displayname = $entry[0][$this->ldap_cn][0]; + if (!empty($entry[0][$this->ldap_mail])) + $user_email = $entry[0][$this->ldap_mail][0]; + + $user = new \Baikal\Model\User(); + $user->set('username', $username); + $user->set('displayname', $user_displayname); + $user->set('email', $user_email); + $user->persist(); + } + } + + ldap_close($conn); + + return $success; + } + + /** + * Validates a username and password by trying to authenticate against LDAP. + * + * @param string $username + * @param string $password + * + * @return bool + */ + protected function validateUserPass($username, $password) + { + return $this->ldapOpen($username, $password); + } +} diff --git a/Core/Frameworks/Baikal/Core/Server.php b/Core/Frameworks/Baikal/Core/Server.php index f08c1100..39434a87 100644 --- a/Core/Frameworks/Baikal/Core/Server.php +++ b/Core/Frameworks/Baikal/Core/Server.php @@ -135,6 +135,8 @@ protected function initServer() { $authBackend = new \Baikal\Core\PDOBasicAuth($this->pdo, $this->authRealm); } elseif ($this->authType === 'Apache') { $authBackend = new \Sabre\DAV\Auth\Backend\Apache(); + } elseif ($this->authType === 'LDAP') { + $authBackend = new \Baikal\Core\LDAP($this->pdo, 'users', $config['system']['ldap_mode'], $config['system']['ldap_uri'], $config['system']['ldap_bind_dn'], $config['system']['ldap_bind_password'], $config['system']['ldap_dn'], $config['system']['ldap_cn'], $config['system']['ldap_mail'], $config['system']['ldap_search_base'], $config['system']['ldap_search_attribute'], $config['system']['ldap_search_filter'], $config['system']['ldap_group']); } else { $authBackend = new \Sabre\DAV\Auth\Backend\PDO($this->pdo); $authBackend->setRealm($this->authRealm); @@ -174,7 +176,12 @@ protected function initServer() { $this->server->addPlugin(new \Sabre\DAV\Sharing\Plugin()); $this->server->addPlugin(new \Sabre\CalDAV\SharingPlugin()); if (isset($config['system']["invite_from"]) && $config['system']["invite_from"] !== "") { - $this->server->addPlugin(new \Sabre\CalDAV\Schedule\IMipPlugin($config['system']["invite_from"])); + if($config['system']['use_smtp']) { + $this->server->addPlugin(new \Baikal\Core\IMipSMTPPlugin($config['system']["invite_from"], $config['system']['smtp_host'], $config['system']['smtp_port'], $config['system']['smtp_username'], $config['system']['smtp_password'])); + } + else { + $this->server->addPlugin(new \Sabre\CalDAV\Schedule\IMipPlugin($config['system']["invite_from"])); + } } } if ($this->enableCardDAV) { diff --git a/Core/Frameworks/Baikal/Model/Config/Standard.php b/Core/Frameworks/Baikal/Model/Config/Standard.php index 310d512d..5a32437f 100644 --- a/Core/Frameworks/Baikal/Model/Config/Standard.php +++ b/Core/Frameworks/Baikal/Model/Config/Standard.php @@ -32,17 +32,33 @@ class Standard extends \Baikal\Model\Config { # Default values protected $aData = [ - "configured_version" => BAIKAL_VERSION, - "timezone" => "Europe/Paris", - "card_enabled" => true, - "cal_enabled" => true, - "dav_auth_type" => "Digest", - "admin_passwordhash" => "", - "failed_access_message" => "user %u authentication failure for Baikal", + "configured_version" => BAIKAL_VERSION, + "timezone" => "Europe/Paris", + "card_enabled" => true, + "cal_enabled" => true, + "dav_auth_type" => "Digest", + "ldap_mode" => "None", + "ldap_uri" => "ldap://127.0.0.1", + "ldap_bind_dn" => "cn=baikal,ou=apps,dc=example,dc=com", + "ldap_bind_password" => "", + "ldap_dn" => "mail=%u", + "ldap_cn" => "cn", + "ldap_mail" => "mail", + "ldap_search_base" => "ou=users,dc=example,dc=com", + "ldap_search_attribute" => "uid=%U", + "ldap_search_filter" => "(objectClass=*)", + "ldap_group" => "cn=baikal,ou=groups,dc=example,dc=com", + "use_smtp" => false, + "smtp_username" => "", + "smtp_password" => "", + "smtp_host" => "", + "smtp_port" => "465", + "admin_passwordhash" => "", + "failed_access_message" => "user %u authentication failure for Baikal", // While not editable as will change admin & any existing user passwords, // could be set to different value when migrating from legacy config - "auth_realm" => "BaikalDAV", - "base_uri" => "", + "auth_realm" => "BaikalDAV", + "base_uri" => "", ]; function __construct() { @@ -76,10 +92,95 @@ function formMorphologyForThisModelInstance() { "help" => "Leave empty to disable sending invite emails", ])); + $oMorpho->add(new \Formal\Element\Checkbox([ + "prop" => "use_smtp", + "label" => "Use SMTP for sending emails", + "refreshonchange" => true, + ])); + + $oMorpho->add(new \Formal\Element\Text([ + "prop" => "smtp_username", + "label" => "Username for SMTP server", + ])); + + $oMorpho->add(new \Formal\Element\Text([ + "prop" => "smtp_password", + "label" => "Password for SMTP server", + ])); + + $oMorpho->add(new \Formal\Element\Text([ + "prop" => "smtp_host", + "label" => "SMTP server address", + ])); + + $oMorpho->add(new \Formal\Element\Text([ + "prop" => "smtp_port", + "label" => "SMTP server port", + ])); + $oMorpho->add(new \Formal\Element\Listbox([ "prop" => "dav_auth_type", "label" => "WebDAV authentication type", - "options" => ["Digest", "Basic", "Apache"], + "options" => ["Digest", "Basic", "Apache", "LDAP"], + "refreshonchange" => true, + ])); + + $oMorpho->add(new \Formal\Element\Listbox([ + "prop" => "ldap_mode", + "label" => "LDAP authentication mode", + "options" => ["DN", "Attribute", "Filter", "Group"], + "refreshonchange" => true, + ])); + + $oMorpho->add(new \Formal\Element\Text([ + "prop" => "ldap_uri", + "label" => "URI of the LDAP server; default ldap://127.0.0.1", + ])); + + $oMorpho->add(new \Formal\Element\Text([ + "prop" => "ldap_bind_dn", + "label" => "DN which Baikal will use to bind to the LDAP server", + ])); + + $oMorpho->add(new \Formal\Element\Password([ + "prop" => "ldap_bind_password", + "label" => "The password of the bind DN user", + ])); + + $oMorpho->add(new \Formal\Element\Text([ + "prop" => "ldap_dn", + "label" => "User DN for bind; with replacments %u => username, %U => user part, %d => domain part of username, %1-9 parts of the domain in reverse order", + ])); + + $oMorpho->add(new \Formal\Element\Text([ + "prop" => "ldap_cn", + "label" => "LDAP-attribute for displayname; default cn", + ])); + + $oMorpho->add(new \Formal\Element\Text([ + "prop" => "ldap_mail", + "label" => "LDAP-attribute for email; default mail", + ])); + + $oMorpho->add(new \Formal\Element\Text([ + "prop" => "ldap_search_base", + "label" => "The base of the LDAP search", + ])); + + + $oMorpho->add(new \Formal\Element\Text([ + "prop" => "ldap_search_attribute", + "label" => "Attribute and match the user with.; with replacments %u => username, %U => user part, %d => domain part of username, %1-9 parts of the domain in reverse order", + ])); + + $oMorpho->add(new \Formal\Element\Text([ + "prop" => "ldap_search_filter", + "label" => "The LDAP filter to be applied to the search.", + ])); + + $oMorpho->add(new \Formal\Element\Text([ + "prop" => "ldap_group", + "label" => "The Group DN that contains the member atribute of the user.", ])); $oMorpho->add(new \Formal\Element\Password([ diff --git a/Core/Frameworks/BaikalAdmin/Controller/Settings/Standard.php b/Core/Frameworks/BaikalAdmin/Controller/Settings/Standard.php index 204f31b8..7b7b14a8 100644 --- a/Core/Frameworks/BaikalAdmin/Controller/Settings/Standard.php +++ b/Core/Frameworks/BaikalAdmin/Controller/Settings/Standard.php @@ -27,6 +27,8 @@ namespace BaikalAdmin\Controller\Settings; +use Symfony\Component\Yaml\Yaml; + class Standard extends \Flake\Core\Controller { /** * @var \Baikal\Model\Config\Standard @@ -48,6 +50,7 @@ function execute() { $this->oForm = $this->oModel->formForThisModelInstance([ "close" => false, + "hook.morphology" => [$this, "morphologyHook"], ]); if ($this->oForm->submitted()) { @@ -61,4 +64,64 @@ function render() { return $oView->render(); } + + function morphologyHook(\Formal\Form $oForm, \Formal\Form\Morphology $oMorpho) { + if ($oForm->submitted()) { + $bSMTP = (intval($oForm->postValue("use_smtp")) === 1); + $bLDAP = (strval($oForm->postValue("dav_auth_type")) === "LDAP"); + $sLDAPm = strval($oForm->postValue("ldap_mode")); + } else { + try { + $config = Yaml::parseFile(PROJECT_PATH_CONFIG . "baikal.yaml"); + } catch (\Exception $e) { + error_log('Error reading baikal.yaml file : ' . $e->getMessage()); + } + $bSMTP = $config['system']['use_smtp'] ?? true; + $bLDAP = isset($config['system']['dav_auth_type']) ? ($config['system']['dav_auth_type'] === 'LDAP') : false; + $sLDAPm = $config['system']['ldap_mode'] ?? 'DN'; + } + + if ($bSMTP) { + $oMorpho->remove("invite_from"); + } else { + $oMorpho->remove("smtp_username"); + $oMorpho->remove("smtp_password"); + $oMorpho->remove("smtp_host"); + $oMorpho->remove("smtp_port"); + } + if ($bLDAP) { + if ($sLDAPm === "DN") { + $oMorpho->remove("ldap_bind_dn"); + $oMorpho->remove("ldap_bind_password"); + $oMorpho->remove("ldap_search_base"); + $oMorpho->remove("ldap_search_attribute"); + $oMorpho->remove("ldap_search_filter"); + $oMorpho->remove("ldap_group"); + } elseif ($sLDAPm === "Attribute") { + $oMorpho->remove("ldap_dn"); + $oMorpho->remove("ldap_search_filter"); + $oMorpho->remove("ldap_group"); + } elseif ($sLDAPm === "Filter") { + $oMorpho->remove("ldap_dn"); + $oMorpho->remove("ldap_search_attribute"); + $oMorpho->remove("ldap_group"); + } elseif ($sLDAPm === "Group") { + $oMorpho->remove("ldap_dn"); + $oMorpho->remove("ldap_search_filter"); + } else { + error_log('Unknown LDAP mode: '.$sLDAPm); + } + } else { + $oMorpho->remove("ldap_mode"); + $oMorpho->remove("ldap_bind_dn"); + $oMorpho->remove("ldap_bind_password"); + $oMorpho->remove("ldap_dn"); + $oMorpho->remove("ldap_cn"); + $oMorpho->remove("ldap_mail"); + $oMorpho->remove("ldap_search_base"); + $oMorpho->remove("ldap_search_attribute"); + $oMorpho->remove("ldap_search_filter"); + $oMorpho->remove("ldap_group"); + } + } } diff --git a/Core/Frameworks/BaikalAdmin/Resources/main.js b/Core/Frameworks/BaikalAdmin/Resources/main.js index 0414853a..1cbd9d9c 100644 --- a/Core/Frameworks/BaikalAdmin/Resources/main.js +++ b/Core/Frameworks/BaikalAdmin/Resources/main.js @@ -24,4 +24,4 @@ function copyToClipboard(el) { sel.removeAllRanges(); $(el).css({backgroundColor:"#75c753"}); $(el).animate({backgroundColor:"transparent"}, 1500); -} +} \ No newline at end of file diff --git a/Core/Frameworks/Formal/Element/Checkbox.php b/Core/Frameworks/Formal/Element/Checkbox.php index 39649c46..fe800b9d 100644 --- a/Core/Frameworks/Formal/Element/Checkbox.php +++ b/Core/Frameworks/Formal/Element/Checkbox.php @@ -82,4 +82,4 @@ function render() { return $sHtml . $this->renderWitness(); } -} +} \ No newline at end of file diff --git a/Core/Frameworks/Formal/Element/Listbox.php b/Core/Frameworks/Formal/Element/Listbox.php index f398965c..a241cd5d 100644 --- a/Core/Frameworks/Formal/Element/Listbox.php +++ b/Core/Frameworks/Formal/Element/Listbox.php @@ -39,6 +39,7 @@ function render() { $prop = $this->option("prop"); $helpblock = ""; $popover = ""; + $onchange = ""; if ($this->option("readonly") === true) { $inputclass .= " disabled"; @@ -64,6 +65,10 @@ function render() { $popover .= " data-content=\"" . htmlspecialchars($aPopover["content"]) . "\" "; } + if ($this->option("refreshonchange") === true) { + $onchange = " onchange=\"document.getElementsByTagName('form')[0].elements['refreshed'].value=1;document.getElementsByTagName('form')[0].submit();\" "; + } + $clientvalue = htmlspecialchars($value); $aRenderedOptions = []; @@ -92,7 +97,7 @@ function render() {
- {$sRenderedOptions} {$helpblock}