* All rights reserved. * * originally based on m0n0wall (http://m0n0.ch/wall) * Copyright (c) 2003-2004 Manuel Kasper . * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. All advertising materials mentioning features or use of this software * must display the following acknowledgment: * "This product includes software developed by the pfSense Project * for use in the pfSense® software distribution. (http://www.pfsense.org/). * * 4. The names "pfSense" and "pfSense Project" must not be used to * endorse or promote products derived from this software without * prior written permission. For written permission, please contact * coreteam@pfsense.org. * * 5. Products derived from this software may not be called "pfSense" * nor may "pfSense" appear in their names without prior written * permission of the Electric Sheep Fencing, LLC. * * 6. Redistributions of any form whatsoever must retain the following * acknowledgment: * * "This product includes software developed by the pfSense Project * for use in the pfSense software distribution (http://www.pfsense.org/). * * THIS SOFTWARE IS PROVIDED BY THE pfSense PROJECT ``AS IS'' AND ANY * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE pfSense PROJECT OR * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED * OF THE POSSIBILITY OF SUCH DAMAGE. */ ##|+PRIV ##|*IDENT=page-system-usermanager ##|*NAME=System: User Manager ##|*DESCR=Allow access to the 'System: User Manager' page. ##|*MATCH=system_usermanager.php* ##|-PRIV require_once("certs.inc"); require_once("guiconfig.inc"); // start admin user code if (isset($_POST['userid']) && is_numericint($_POST['userid'])) { $id = $_POST['userid']; } if (isset($_GET['userid']) && is_numericint($_GET['userid'])) { $id = $_GET['userid']; } if (!isset($config['system']['user']) || !is_array($config['system']['user'])) { $config['system']['user'] = array(); } $a_user = &$config['system']['user']; $act = $_GET['act']; if (isset($_SERVER['HTTP_REFERER'])) { $referer = $_SERVER['HTTP_REFERER']; } else { $referer = '/system_usermanager.php'; } if (isset($id) && $a_user[$id]) { $pconfig['usernamefld'] = $a_user[$id]['name']; $pconfig['descr'] = $a_user[$id]['descr']; $pconfig['expires'] = $a_user[$id]['expires']; $pconfig['customsettings'] = isset($a_user[$id]['customsettings']); $pconfig['webguicss'] = $a_user[$id]['webguicss']; $pconfig['webguifixedmenu'] = $a_user[$id]['webguifixedmenu']; $pconfig['webguihostnamemenu'] = $a_user[$id]['webguihostnamemenu']; $pconfig['dashboardcolumns'] = $a_user[$id]['dashboardcolumns']; $pconfig['dashboardavailablewidgetspanel'] = isset($a_user[$id]['dashboardavailablewidgetspanel']); $pconfig['systemlogsfilterpanel'] = isset($a_user[$id]['systemlogsfilterpanel']); $pconfig['systemlogsmanagelogpanel'] = isset($a_user[$id]['systemlogsmanagelogpanel']); $pconfig['statusmonitoringsettingspanel'] = isset($a_user[$id]['statusmonitoringsettingspanel']); $pconfig['webguileftcolumnhyper'] = isset($a_user[$id]['webguileftcolumnhyper']); $pconfig['pagenamefirst'] = isset($a_user[$id]['pagenamefirst']); $pconfig['groups'] = local_user_get_groups($a_user[$id]); $pconfig['utype'] = $a_user[$id]['scope']; $pconfig['uid'] = $a_user[$id]['uid']; $pconfig['authorizedkeys'] = base64_decode($a_user[$id]['authorizedkeys']); $pconfig['priv'] = $a_user[$id]['priv']; $pconfig['ipsecpsk'] = $a_user[$id]['ipsecpsk']; $pconfig['disabled'] = isset($a_user[$id]['disabled']); } if ($_GET['act'] == "deluser") { if (!isset($_GET['username']) || !isset($a_user[$id]) || ($_GET['username'] != $a_user[$id]['name'])) { pfSenseHeader("system_usermanager.php"); exit; } if ($_GET['username'] == $_SESSION['Username']) { $delete_errors[] = sprintf(gettext("Cannot delete user %s because you are currently logged in as that user."), $_GET['username']); } else { conf_mount_rw(); local_user_del($a_user[$id]); conf_mount_ro(); $userdeleted = $a_user[$id]['name']; unset($a_user[$id]); write_config(); $savemsg = sprintf(gettext("User %s successfully deleted."), $userdeleted); } } else if ($act == "new") { /* * set this value cause the text field is read only * and the user should not be able to mess with this * setting. */ $pconfig['utype'] = "user"; $pconfig['lifetime'] = 3650; } if (isset($_POST['dellall'])) { $del_users = $_POST['delete_check']; $deleted_users = ""; $deleted_count = 0; $comma = ""; if (!empty($del_users)) { foreach ($del_users as $userid) { if (isset($a_user[$userid]) && $a_user[$userid]['scope'] != "system") { if ($a_user[$userid]['name'] == $_SESSION['Username']) { $delete_errors[] = sprintf(gettext("Cannot delete user %s because you are currently logged in as that user."), $a_user[$userid]['name']); } else { conf_mount_rw(); $deleted_users = $deleted_users . $comma . $a_user[$userid]['name']; $comma = ", "; $deleted_count++; local_user_del($a_user[$userid]); conf_mount_ro(); unset($a_user[$userid]); } } else { $delete_errors[] = sprintf(gettext("Cannot delete user %s because it is a system user."), $a_user[$userid]['name']); } } if ($deleted_count > 0) { if ($deleted_count == 1) { $savemsg = sprintf(gettext("User %s successfully deleted."), $deleted_users); } else { $savemsg = sprintf(gettext("Users %s successfully deleted."), $deleted_users); } write_config($savemsg); } } } if ($_POST['act'] == "delcert") { if (!$a_user[$id]) { pfSenseHeader("system_usermanager.php"); exit; } $certdeleted = lookup_cert($a_user[$id]['cert'][$_POST['certid']]); $certdeleted = $certdeleted['descr']; unset($a_user[$id]['cert'][$_POST['certid']]); write_config(); $_POST['act'] = "edit"; $savemsg = sprintf(gettext("Certificate %s association removed."), $certdeleted); } if ($_POST['act'] == "delprivid") { $privdeleted = $priv_list[$a_user[$id]['priv'][$_POST['privid']]]['name']; unset($a_user[$id]['priv'][$_POST['privid']]); local_user_set($a_user[$id]); write_config(); $_POST['act'] = "edit"; $savemsg = sprintf(gettext("Privilege %s removed."), $privdeleted); } if ($_POST['save']) { unset($input_errors); $pconfig = $_POST; /* input validation */ if (isset($id) && ($a_user[$id])) { $reqdfields = explode(" ", "usernamefld"); $reqdfieldsn = array(gettext("Username")); } else { if (empty($_POST['name'])) { $reqdfields = explode(" ", "usernamefld passwordfld1"); $reqdfieldsn = array( gettext("Username"), gettext("Password")); } else { $reqdfields = explode(" ", "usernamefld passwordfld1 name caref keylen lifetime"); $reqdfieldsn = array( gettext("Username"), gettext("Password"), gettext("Descriptive name"), gettext("Certificate authority"), gettext("Key length"), gettext("Lifetime")); } } do_input_validation($_POST, $reqdfields, $reqdfieldsn, $input_errors); if (preg_match("/[^a-zA-Z0-9\.\-_]/", $_POST['usernamefld'])) { $input_errors[] = gettext("The username contains invalid characters."); } if (strlen($_POST['usernamefld']) > 16) { $input_errors[] = gettext("The username is longer than 16 characters."); } if (($_POST['passwordfld1']) && ($_POST['passwordfld1'] != $_POST['passwordfld2'])) { $input_errors[] = gettext("The passwords do not match."); } if (isset($_POST['ipsecpsk']) && !preg_match('/^[[:ascii:]]*$/', $_POST['ipsecpsk'])) { $input_errors[] = gettext("IPsec Pre-Shared Key contains invalid characters."); } /* Check the POSTed groups to ensure they are valid and exist */ if (is_array($_POST['groups'])) { foreach ($_POST['groups'] as $newgroup) { if (empty(getGroupEntry($newgroup))) { $input_errors[] = gettext("One or more invalid groups was submitted."); } } } if (isset($id) && $a_user[$id]) { $oldusername = $a_user[$id]['name']; } else { $oldusername = ""; } /* make sure this user name is unique */ if (!$input_errors) { foreach ($a_user as $userent) { if ($userent['name'] == $_POST['usernamefld'] && $oldusername != $_POST['usernamefld']) { $input_errors[] = gettext("Another entry with the same username already exists."); break; } } } /* also make sure it is not reserved */ if (!$input_errors) { $system_users = explode("\n", file_get_contents("/etc/passwd")); foreach ($system_users as $s_user) { $ent = explode(":", $s_user); if ($ent[0] == $_POST['usernamefld'] && $oldusername != $_POST['usernamefld']) { $input_errors[] = gettext("That username is reserved by the system."); break; } } } /* * Check for a valid expiration date if one is set at all (valid means, * DateTime puts out a time stamp so any DateTime compatible time * format may be used. to keep it simple for the enduser, we only * claim to accept MM/DD/YYYY as inputs. Advanced users may use inputs * like "+1 day", which will be converted to MM/DD/YYYY based on "now". * Otherwise such an entry would lead to an invalid expiration data. */ if ($_POST['expires']) { try { $expdate = new DateTime($_POST['expires']); //convert from any DateTime compatible date to MM/DD/YYYY $_POST['expires'] = $expdate->format("m/d/Y"); } catch (Exception $ex) { $input_errors[] = gettext("Invalid expiration date format; use MM/DD/YYYY instead."); } } if (!empty($_POST['name'])) { $ca = lookup_ca($_POST['caref']); if (!$ca) { $input_errors[] = gettext("Invalid internal Certificate Authority") . "\n"; } } /* if this is an AJAX caller then handle via JSON */ if (isAjax() && is_array($input_errors)) { input_errors2Ajax($input_errors); exit; } if (!$input_errors) { conf_mount_rw(); $userent = array(); if (isset($id) && $a_user[$id]) { $userent = $a_user[$id]; } isset($_POST['utype']) ? $userent['scope'] = $_POST['utype'] : $userent['scope'] = "system"; /* the user name was modified */ if (!empty($_POST['oldusername']) && ($_POST['usernamefld'] <> $_POST['oldusername'])) { $_SERVER['REMOTE_USER'] = $_POST['usernamefld']; local_user_del($userent); } /* the user password was modified */ if ($_POST['passwordfld1']) { local_user_set_password($userent, $_POST['passwordfld1']); } /* only change description if sent */ if (isset($_POST['descr'])) { $userent['descr'] = $_POST['descr']; } $userent['name'] = $_POST['usernamefld']; $userent['expires'] = $_POST['expires']; $userent['dashboardcolumns'] = $_POST['dashboardcolumns']; $userent['authorizedkeys'] = base64_encode($_POST['authorizedkeys']); $userent['ipsecpsk'] = $_POST['ipsecpsk']; if ($_POST['disabled']) { $userent['disabled'] = true; } else { unset($userent['disabled']); } if ($_POST['customsettings']) { $userent['customsettings'] = true; } else { unset($userent['customsettings']); } if ($_POST['webguicss']) { $userent['webguicss'] = $_POST['webguicss']; } else { unset($userent['webguicss']); } if ($_POST['webguifixedmenu']) { $userent['webguifixedmenu'] = $_POST['webguifixedmenu']; } else { unset($userent['webguifixedmenu']); } if ($_POST['webguihostnamemenu']) { $userent['webguihostnamemenu'] = $_POST['webguihostnamemenu']; } else { unset($userent['webguihostnamemenu']); } if ($_POST['dashboardavailablewidgetspanel']) { $userent['dashboardavailablewidgetspanel'] = true; } else { unset($userent['dashboardavailablewidgetspanel']); } if ($_POST['systemlogsfilterpanel']) { $userent['systemlogsfilterpanel'] = true; } else { unset($userent['systemlogsfilterpanel']); } if ($_POST['systemlogsmanagelogpanel']) { $userent['systemlogsmanagelogpanel'] = true; } else { unset($userent['systemlogsmanagelogpanel']); } if ($_POST['statusmonitoringsettingspanel']) { $userent['statusmonitoringsettingspanel'] = true; } else { unset($userent['statusmonitoringsettingspanel']); } if ($_POST['webguileftcolumnhyper']) { $userent['webguileftcolumnhyper'] = true; } else { unset($userent['webguileftcolumnhyper']); } if ($_POST['pagenamefirst']) { $userent['pagenamefirst'] = true; } else { unset($userent['pagenamefirst']); } if (isset($id) && $a_user[$id]) { $a_user[$id] = $userent; } else { if (!empty($_POST['name'])) { $cert = array(); $cert['refid'] = uniqid(); $userent['cert'] = array(); $cert['descr'] = $_POST['name']; $subject = cert_get_subject_array($ca['crt']); $dn = array( 'countryName' => $subject[0]['v'], 'stateOrProvinceName' => $subject[1]['v'], 'localityName' => $subject[2]['v'], 'organizationName' => $subject[3]['v'], 'emailAddress' => $subject[4]['v'], 'commonName' => $userent['name']); cert_create($cert, $_POST['caref'], $_POST['keylen'], (int)$_POST['lifetime'], $dn); if (!is_array($config['cert'])) { $config['cert'] = array(); } $config['cert'][] = $cert; $userent['cert'][] = $cert['refid']; } $userent['uid'] = $config['system']['nextuid']++; /* Add the user to All Users group. */ foreach ($config['system']['group'] as $gidx => $group) { if ($group['name'] == "all") { if (!is_array($config['system']['group'][$gidx]['member'])) { $config['system']['group'][$gidx]['member'] = array(); } $config['system']['group'][$gidx]['member'][] = $userent['uid']; break; } } $a_user[] = $userent; } /* Add user to groups so PHP can see the memberships properly or else the user's shell account does not get proper permissions (if applicable) See #5152. */ local_user_set_groups($userent, $_POST['groups']); local_user_set($userent); /* Add user to groups again to ensure they are set everywhere, otherwise the user may not appear to be a member of the group. See commit:5372d26d9d25d751d16865ed9d46869d3b0ec5e1. */ local_user_set_groups($userent, $_POST['groups']); write_config(); if (is_dir("/etc/inc/privhooks")) { run_plugins("/etc/inc/privhooks"); } conf_mount_ro(); pfSenseHeader("system_usermanager.php"); } } function build_priv_table() { global $a_user, $id; $privhtml = '
'; $privhtml .= ''; $privhtml .= ''; $privhtml .= ''; $privhtml .= ''; $privhtml .= ''; $privhtml .= ''; $privhtml .= ''; $privhtml .= ''; $privhtml .= ''; $privhtml .= ''; $i = 0; foreach (get_user_privdesc($a_user[$id]) as $priv) { $group = false; if ($priv['group']) { $group = $priv['group']; } $privhtml .= ''; $privhtml .= ''; $privhtml .= ''; $privhtml .= ''; $privhtml .= ''; $privhtml .= ''; if (!$group) { $i++; } } $privhtml .= ''; $privhtml .= '
' . gettext('Inherited from') . '' . gettext('Name') . '' . gettext('Description') . '' . gettext('Action') . '
' . htmlspecialchars($priv['group']) . '' . htmlspecialchars($priv['name']) . '' . htmlspecialchars($priv['descr']) . ''; if (!$group) { $privhtml .= ''; } $privhtml .= '
'; $privhtml .= '
'; $privhtml .= ''; return($privhtml); } function build_cert_table() { global $a_user, $id; $certhtml = '
'; $certhtml .= ''; $certhtml .= ''; $certhtml .= ''; $certhtml .= ''; $certhtml .= ''; $certhtml .= ''; $certhtml .= ''; $certhtml .= ''; $certhtml .= ''; $a_cert = $a_user[$id]['cert']; if (is_array($a_cert)) { $i = 0; foreach ($a_cert as $certref) { $cert = lookup_cert($certref); $ca = lookup_ca($cert['caref']); $revokedstr = is_cert_revoked($cert) ? ' Revoked':''; $certhtml .= ''; $certhtml .= ''; $certhtml .= ''; $certhtml .= ''; $certhtml .= ''; $i++; } } $certhtml .= ''; $certhtml .= '
' . gettext('Name') . '' . gettext('CA') . '
' . htmlspecialchars($cert['descr']) . $revokedstr . '' . htmlspecialchars($ca['descr']) . ''; $certhtml .= ''; $certhtml .= '
'; $certhtml .= '
'; $certhtml .= ''; return($certhtml); } $pgtitle = array(gettext("System"), gettext("User Manager"), gettext("Users")); if ($act == "new" || $act == "edit" || $input_errors) { $pgtitle[] = gettext('Edit'); } include("head.inc"); if ($delete_errors) { print_input_errors($delete_errors); } if ($input_errors) { print_input_errors($input_errors); } if ($savemsg) { print_info_box($savemsg, 'success'); } $tab_array = array(); $tab_array[] = array(gettext("Users"), true, "system_usermanager.php"); $tab_array[] = array(gettext("Groups"), false, "system_groupmanager.php"); $tab_array[] = array(gettext("Settings"), false, "system_usermanager_settings.php"); $tab_array[] = array(gettext("Authentication Servers"), false, "system_authservers.php"); display_top_tabs($tab_array); if (!($act == "new" || $act == "edit" || $input_errors)) { ?>
' . gettext("Additional users can be added here. User permissions for accessing " . "the webConfigurator can be assigned directly or inherited from group memberships. " . "Some system object properties can be modified but they cannot be deleted.") . '

' . '

' . gettext("Accounts added here are also used for other parts of the system " . "such as OpenVPN, IPsec, and Captive Portal.") . '

' ); ?>
addGlobal(new Form_Input( 'act', null, 'hidden', '' )); $form->addGlobal(new Form_Input( 'userid', null, 'hidden', isset($id) ? $id:'' )); $form->addGlobal(new Form_Input( 'privid', null, 'hidden', '' )); $form->addGlobal(new Form_Input( 'certid', null, 'hidden', '' )); $ro = ""; if ($pconfig['utype'] == "system") { $ro = "readonly"; } $section = new Form_Section('User Properties'); $section->addInput(new Form_StaticText( 'Defined by', strtoupper($pconfig['utype']) )); $form->addGlobal(new Form_Input( 'utype', null, 'hidden', $pconfig['utype'] )); $section->addInput(new Form_Checkbox( 'disabled', 'Disabled', 'This user cannot login', $pconfig['disabled'] )); $section->addInput($input = new Form_Input( 'usernamefld', 'Username', 'text', $pconfig['usernamefld'] )); if ($ro) { $input->setReadonly(); } $form->addGlobal(new Form_Input( 'oldusername', null, 'hidden', $pconfig['usernamefld'] )); $group = new Form_Group('Password'); $group->add(new Form_Input( 'passwordfld1', 'Password', 'password' )); $group->add(new Form_Input( 'passwordfld2', 'Confirm Password', 'password' )); $section->add($group); $section->addInput($input = new Form_Input( 'descr', 'Full name', 'text', htmlspecialchars($pconfig['descr']) ))->setHelp('User\'s full name, for administrative information only'); if ($ro) { $input->setDisabled(); } $section->addInput(new Form_Input( 'expires', 'Expiration date', 'text', $pconfig['expires'] ))->setHelp('Leave blank if the account shouldn\'t expire, otherwise enter '. 'the expiration date'); $section->addInput(new Form_Checkbox( 'customsettings', 'Custom Settings', 'Use individual customized GUI options and dashboard layout for this user.', $pconfig['customsettings'] )); gen_user_settings_fields($section, $pconfig); // ==== Group membership ================================================== $group = new Form_Group('Group membership'); // Make a list of all the groups configured on the system, and a list of // those which this user is a member of $systemGroups = array(); $usersGroups = array(); $usergid = [$pconfig['usernamefld']]; foreach ($config['system']['group'] as $Ggroup) { if ($Ggroup['name'] != "all") { if (($act == 'edit') && $Ggroup['member'] && in_array($pconfig['uid'], $Ggroup['member'])) { $usersGroups[ $Ggroup['name'] ] = $Ggroup['name']; // Add it to the user's list } else { $systemGroups[ $Ggroup['name'] ] = $Ggroup['name']; // Add it to the 'not a member of' list } } } $group->add(new Form_Select( 'sysgroups', null, array_combine((array)$pconfig['groups'], (array)$pconfig['groups']), $systemGroups, true ))->setHelp('Not member of'); $group->add(new Form_Select( 'groups', null, array_combine((array)$pconfig['groups'], (array)$pconfig['groups']), $usersGroups, true ))->setHelp('Member of'); $section->add($group); $group = new Form_Group(''); $group->add(new Form_Button( 'movetoenabled', 'Move to "Member of" list', null, 'fa-angle-double-right' ))->setAttribute('type','button')->removeClass('btn-primary')->addClass('btn-info btn-sm'); $group->add(new Form_Button( 'movetodisabled', 'Move to "Not member of" list', null, 'fa-angle-double-left' ))->setAttribute('type','button')->removeClass('btn-primary')->addClass('btn-info btn-sm'); $group->setHelp('Hold down CTRL (PC)/COMMAND (Mac) key to select multiple items.'); $section->add($group); // ==== Button for adding user certificate ================================ if ($act == 'new') { $section->addInput(new Form_Checkbox( 'showcert', 'Certificate', 'Click to create a user certificate', false )); } $form->add($section); // ==== Effective privileges section ====================================== if (isset($pconfig['uid'])) { // We are going to build an HTML table and add it to an Input_StaticText. It may be ugly, but it // is the best way to make the display we need. $section = new Form_Section('Effective Privileges'); $section->addInput(new Form_StaticText( null, build_priv_table() )); $form->add($section); // ==== Certificate table section ===================================== $section = new Form_Section('User Certificates'); $section->addInput(new Form_StaticText( null, build_cert_table() )); $form->add($section); } // ==== Add user certificate for a new user if (is_array($config['ca']) && count($config['ca']) > 0) { $section = new Form_Section('Create Certificate for User'); $section->addClass('cert-options'); $nonPrvCas = array(); foreach ($config['ca'] as $ca) { if (!$ca['prv']) { continue; } $nonPrvCas[ $ca['refid'] ] = $ca['descr']; } if (!empty($nonPrvCas)) { $section->addInput(new Form_Input( 'name', 'Descriptive name', 'text', $pconfig['name'] )); $section->addInput(new Form_Select( 'caref', 'Certificate authority', null, $nonPrvCas )); $section->addInput(new Form_Select( 'keylen', 'Key length', 2048, array( 512 => '512 bits', 1024 => '1024 bits', 2048 => '2048 bits', 4096 => '4096 bits', ) )); $section->addInput(new Form_Input( 'lifetime', 'Lifetime', 'number', $pconfig['lifetime'] )); } $form->add($section); } endif; // ==== Paste a key for the new user $section = new Form_Section('Keys'); $section->addInput(new Form_Checkbox( 'showkey', 'Authorized keys', 'Click to paste an authorized key', false )); $section->addInput(new Form_Textarea( 'authorizedkeys', 'Authorized SSH Keys', $pconfig['authorizedkeys'] ))->setHelp('Enter authorized SSH keys for this user'); $section->addInput(new Form_Input( 'ipsecpsk', 'IPsec Pre-Shared Key', 'text', $pconfig['ipsecpsk'] )); $form->add($section); print $form; $csswarning = sprintf(gettext("%sUser-created themes are unsupported, use at your own risk."), "
"); ?>