You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
621 lines
16 KiB
621 lines
16 KiB
/*
|
|
* Copyright (c) 1998 Denis Perchine <dyp@perchine.com>
|
|
* Copyright (c) 2004 Szombathelyi György <gyurco@freemail.hu>
|
|
* Former maintainer: Adriaan de Groot <groot@kde.org>
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public
|
|
* License version 2 as published by the Free Software Foundation.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Library General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Library General Public License
|
|
* along with this library; see the file COPYING.LIB. If not, write to
|
|
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
* Boston, MA 02110-1301, USA.
|
|
**/
|
|
|
|
#include "globals.h"
|
|
#include <errno.h>
|
|
#ifdef HAVE_UNISTD_H
|
|
#include <unistd.h>
|
|
#endif
|
|
#include <sys/types.h>
|
|
#include <sys/file.h>
|
|
#include <sys/stat.h>
|
|
#include <pwd.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#ifdef HAVE_SHADOW
|
|
#include <shadow.h>
|
|
#endif
|
|
|
|
#include <tqstring.h>
|
|
#include <tqdir.h>
|
|
|
|
#include "kglobal_.h"
|
|
#include "kuserfiles.h"
|
|
#include "misc.h"
|
|
#include <kstandarddirs.h>
|
|
#include <tdemessagebox.h>
|
|
#include <kdebug.h>
|
|
#include "editDefaults.h"
|
|
|
|
KUserFiles::KUserFiles(KUserPrefsBase *cfg) : KUsers( cfg )
|
|
{
|
|
pw_backuped = FALSE;
|
|
pn_backuped = FALSE;
|
|
s_backuped = FALSE;
|
|
|
|
pwd_mode = 0644;
|
|
pwd_uid = 0;
|
|
pwd_gid = 0;
|
|
|
|
sdw_mode = 0600;
|
|
sdw_uid = 0;
|
|
sdw_gid = 0;
|
|
|
|
mUsers.setAutoDelete(TRUE);
|
|
|
|
caps = Cap_Passwd;
|
|
#ifdef HAVE_SHADOW
|
|
if ( !mCfg->shadowsrc().isEmpty() ) caps |= Cap_Shadow;
|
|
#endif
|
|
#if defined(__FreeBSD__) || defined(__bsdi__)
|
|
caps |= Cap_BSD;
|
|
#endif
|
|
|
|
reload();
|
|
}
|
|
|
|
KUserFiles::~KUserFiles()
|
|
{
|
|
}
|
|
|
|
bool KUserFiles::reload() {
|
|
if (!loadpwd())
|
|
return FALSE;
|
|
|
|
if (!loadsdw())
|
|
return FALSE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
// Load passwd file
|
|
|
|
bool KUserFiles::loadpwd()
|
|
{
|
|
passwd *p;
|
|
KU::KUser *tmpKU = 0;
|
|
struct stat st;
|
|
TQString filename;
|
|
TQString passwd_filename;
|
|
TQString nispasswd_filename;
|
|
int rc = 0;
|
|
int passwd_errno = 0;
|
|
int nispasswd_errno = 0;
|
|
char processing_file = '\0';
|
|
#define P_PASSWD 0x01
|
|
#define P_NISPASSWD 0x02
|
|
#define MAXFILES 2
|
|
|
|
// Read KUser configuration
|
|
|
|
passwd_filename = mCfg->passwdsrc();
|
|
nispasswd_filename = mCfg->nispasswdsrc();
|
|
|
|
// Handle unconfigured environments
|
|
|
|
if(passwd_filename.isEmpty() && nispasswd_filename.isEmpty()) {
|
|
mCfg->setPasswdsrc( PASSWORD_FILE );
|
|
mCfg->setGroupsrc( GROUP_FILE );
|
|
passwd_filename = mCfg->passwdsrc();
|
|
KMessageBox::error( 0, i18n("KUser sources were not configured.\nLocal passwd source set to %1\nLocal group source set to %2.").arg(mCfg->passwdsrc().arg(mCfg->groupsrc())) );
|
|
}
|
|
|
|
if(!passwd_filename.isEmpty()) {
|
|
processing_file = processing_file | P_PASSWD;
|
|
filename.append(passwd_filename);
|
|
}
|
|
|
|
// Start reading passwd file(s)
|
|
|
|
for(int i = 0; i < MAXFILES; i++) {
|
|
rc = stat(TQFile::encodeName(filename), &st);
|
|
if(rc != 0) {
|
|
KMessageBox::error( 0, i18n("Stat call on file %1 failed: %2\nCheck KUser settings.").arg(filename).arg(TQString::fromLocal8Bit(strerror(errno))) );
|
|
if( (processing_file & P_PASSWD) != 0 ) {
|
|
passwd_errno = errno;
|
|
if(!nispasswd_filename.isEmpty()) {
|
|
processing_file = processing_file & ~P_PASSWD;
|
|
processing_file = processing_file | P_NISPASSWD;
|
|
filename.truncate(0);
|
|
filename.append(nispasswd_filename);
|
|
}
|
|
continue;
|
|
}
|
|
else{
|
|
nispasswd_errno = errno;
|
|
break;
|
|
}
|
|
}
|
|
|
|
pwd_mode = st.st_mode & 0666;
|
|
pwd_uid = st.st_uid;
|
|
pwd_gid = st.st_gid;
|
|
|
|
// We are reading our configuration specified passwd file
|
|
TQString tmp;
|
|
|
|
#ifdef HAVE_FGETPWENT
|
|
FILE *fpwd = fopen(TQFile::encodeName(filename), "r");
|
|
if(fpwd == NULL) {
|
|
KMessageBox::error( 0, i18n("Error opening %1 for reading.").arg(filename) );
|
|
return FALSE;
|
|
}
|
|
|
|
while ((p = fgetpwent(fpwd)) != NULL) {
|
|
#else
|
|
setpwent(); //This should be enough for BSDs
|
|
while ((p = getpwent()) != NULL) {
|
|
#endif
|
|
tmpKU = new KU::KUser();
|
|
tmpKU->setCaps( KU::KUser::Cap_POSIX );
|
|
tmpKU->setUID(p->pw_uid);
|
|
tmpKU->setGID(p->pw_gid);
|
|
tmpKU->setName(TQString::fromLocal8Bit(p->pw_name));
|
|
tmp = TQString::fromLocal8Bit( p->pw_passwd );
|
|
if ( tmp != "x" && tmp != "*" && !tmp.startsWith("!") )
|
|
tmpKU->setDisabled( false );
|
|
else
|
|
tmpKU->setDisabled( true );
|
|
if ( tmp.startsWith("!") ) tmp.remove(0, 1);
|
|
tmpKU->setPwd( tmp );
|
|
tmpKU->setHomeDir(TQString::fromLocal8Bit(p->pw_dir));
|
|
tmpKU->setShell(TQString::fromLocal8Bit(p->pw_shell));
|
|
#if defined(__FreeBSD__) || defined(__bsdi__)
|
|
tmpKU->setClass(TQString::fromLatin1(p->pw_class));
|
|
tmpKU->setLastChange(p->pw_change);
|
|
tmpKU->setExpire(p->pw_expire);
|
|
#endif
|
|
|
|
if ((p->pw_gecos != 0) && (p->pw_gecos[0] != 0))
|
|
fillGecos(tmpKU, p->pw_gecos);
|
|
mUsers.append(tmpKU);
|
|
}
|
|
|
|
// End reading passwd_filename
|
|
|
|
#ifdef HAVE_FGETPWENT
|
|
fclose(fpwd);
|
|
#else
|
|
endpwent();
|
|
#endif
|
|
if((!nispasswd_filename.isEmpty()) && (nispasswd_filename != passwd_filename)) {
|
|
processing_file = processing_file & ~P_PASSWD;
|
|
processing_file = processing_file | P_NISPASSWD;
|
|
filename.truncate(0);
|
|
filename.append(nispasswd_filename);
|
|
}
|
|
else
|
|
break;
|
|
|
|
} // end of processing files, for loop
|
|
|
|
if( (passwd_errno == 0) && (nispasswd_errno == 0) )
|
|
return (TRUE);
|
|
if( (passwd_errno != 0) && (nispasswd_errno != 0) )
|
|
return (FALSE);
|
|
else
|
|
return(TRUE);
|
|
}
|
|
|
|
// Load shadow passwords
|
|
|
|
bool KUserFiles::loadsdw()
|
|
{
|
|
#ifdef HAVE_SHADOW
|
|
TQString shadow_file,tmp;
|
|
struct spwd *spw;
|
|
KU::KUser *up = NULL;
|
|
struct stat st;
|
|
|
|
shadow_file = mCfg->shadowsrc();
|
|
if ( shadow_file.isEmpty() )
|
|
return TRUE;
|
|
|
|
stat( TQFile::encodeName(shadow_file), &st);
|
|
sdw_mode = st.st_mode & 0666;
|
|
sdw_uid = st.st_uid;
|
|
sdw_gid = st.st_gid;
|
|
|
|
#ifdef HAVE_FGETSPENT
|
|
FILE *f;
|
|
kdDebug() << "open shadow file: " << shadow_file << endl;
|
|
if ((f = fopen( TQFile::encodeName(shadow_file), "r")) == NULL) {
|
|
KMessageBox::error( 0, i18n("Error opening %1 for reading.").arg(shadow_file) );
|
|
caps &= ~Cap_Shadow;
|
|
return TRUE;
|
|
}
|
|
while ((spw = fgetspent( f ))) { // read a shadow password structure
|
|
#else
|
|
setspent();
|
|
while ((spw = getspent())) { // read a shadow password structure
|
|
#endif
|
|
|
|
kdDebug() << "shadow entry: " << spw->sp_namp << endl;
|
|
if ((up = lookup(TQString::fromLocal8Bit(spw->sp_namp))) == NULL) {
|
|
KMessageBox::error( 0, i18n("No /etc/passwd entry for %1.\nEntry will be removed at the next `Save'-operation.").arg(TQString::fromLocal8Bit(spw->sp_namp)) );
|
|
continue;
|
|
}
|
|
|
|
tmp = TQString::fromLocal8Bit( spw->sp_pwdp );
|
|
if ( tmp.startsWith("!!") || tmp == "*" ) {
|
|
up->setDisabled( true );
|
|
tmp.remove( 0, 2 );
|
|
} else
|
|
up->setDisabled( false );
|
|
|
|
up->setSPwd( tmp ); // cp the encrypted pwd
|
|
up->setLastChange( daysToTime( spw->sp_lstchg ) );
|
|
up->setMin(spw->sp_min);
|
|
up->setMax(spw->sp_max);
|
|
#ifndef _SCO_DS
|
|
up->setWarn(spw->sp_warn);
|
|
up->setInactive(spw->sp_inact);
|
|
up->setExpire( daysToTime( spw->sp_expire ) );
|
|
up->setFlag(spw->sp_flag);
|
|
#endif
|
|
}
|
|
|
|
#ifdef HAVE_FGETSPENT
|
|
fclose(f);
|
|
#else
|
|
endspent();
|
|
#endif
|
|
|
|
#endif // HAVE_SHADOW
|
|
return TRUE;
|
|
}
|
|
|
|
// Save password file
|
|
|
|
#define escstr(a,b) tmp2 = user->a(); \
|
|
tmp2.replace(':',"_"); \
|
|
tmp2.replace(',',"_"); \
|
|
user->b( tmp2 );
|
|
|
|
|
|
bool KUserFiles::savepwd()
|
|
{
|
|
FILE *passwd_fd = NULL;
|
|
FILE *nispasswd_fd = NULL;
|
|
uid_t minuid = 0;
|
|
int nis_users_written = 0;
|
|
uid_t tmp_uid = 0;
|
|
TQString s;
|
|
TQString s1;
|
|
TQString tmp, tmp2;
|
|
TQString passwd_filename;
|
|
TQString nispasswd_filename;
|
|
|
|
|
|
char errors_found = '\0';
|
|
#define NOMINUID 0x01
|
|
#define NONISPASSWD 0x02
|
|
|
|
// Read KUser configuration info
|
|
|
|
passwd_filename = mCfg->passwdsrc();
|
|
nispasswd_filename = mCfg->nispasswdsrc();
|
|
TQString new_passwd_filename =
|
|
passwd_filename + TQString::fromLatin1(KU_CREATE_EXT);
|
|
TQString new_nispasswd_filename =
|
|
nispasswd_filename+TQString::fromLatin1(KU_CREATE_EXT);
|
|
|
|
if( nispasswd_filename != passwd_filename ) {
|
|
minuid = mCfg->nisminuid();
|
|
}
|
|
|
|
// Backup file(s)
|
|
|
|
if(!passwd_filename.isEmpty()) {
|
|
if (!pw_backuped) {
|
|
if (!backup(passwd_filename)) return FALSE;
|
|
pw_backuped = TRUE;
|
|
}
|
|
}
|
|
if(!nispasswd_filename.isEmpty() &&
|
|
(nispasswd_filename != passwd_filename)) {
|
|
if (!pn_backuped) {
|
|
if (!backup(nispasswd_filename)) return FALSE;
|
|
pn_backuped = TRUE;
|
|
}
|
|
}
|
|
|
|
// Open file(s)
|
|
|
|
if(!passwd_filename.isEmpty()) {
|
|
if ((passwd_fd =
|
|
fopen(TQFile::encodeName(new_passwd_filename),"w")) == NULL)
|
|
KMessageBox::error( 0, i18n("Error opening %1 for writing.").arg(passwd_filename) );
|
|
}
|
|
|
|
if(!nispasswd_filename.isEmpty() && (nispasswd_filename != passwd_filename)){
|
|
if ((nispasswd_fd =
|
|
fopen(TQFile::encodeName(new_nispasswd_filename),"w")) == NULL)
|
|
KMessageBox::error( 0, i18n("Error opening %1 for writing.").arg(nispasswd_filename) );
|
|
}
|
|
|
|
TQPtrListIterator<KU::KUser> it( mUsers );
|
|
KU::KUser *user;
|
|
bool addok = false;
|
|
user = (*it);
|
|
while (true) {
|
|
if ( user == 0 ) {
|
|
if ( addok ) break;
|
|
it = TQPtrListIterator<KU::KUser> ( mAdd );
|
|
user = (*it);
|
|
addok = true;
|
|
if ( user == 0 ) break;
|
|
};
|
|
if ( mDel.containsRef( user ) ) {
|
|
++it;
|
|
user = (*it);
|
|
continue;
|
|
}
|
|
if ( mMod.contains( user ) ) user = &( mMod[ user ] );
|
|
|
|
tmp_uid = user->getUID();
|
|
if ( caps & Cap_Shadow )
|
|
tmp = "x";
|
|
else {
|
|
tmp = user->getPwd();
|
|
if ( user->getDisabled() && tmp != "x" && tmp != "*" )
|
|
tmp = "!" + tmp;
|
|
}
|
|
|
|
escstr( getName, setName );
|
|
escstr( getHomeDir, setHomeDir );
|
|
escstr( getShell, setShell );
|
|
escstr( getName, setName );
|
|
escstr( getFullName, setFullName );
|
|
#if defined(__FreeBSD__) || defined(__bsdi__)
|
|
escstr( getClass, setClass );
|
|
escstr( getOffice, setOffice );
|
|
escstr( getWorkPhone, setWorkPhone );
|
|
escstr( getHomePhone, setHomePhone );
|
|
s =
|
|
user->getName() + ":" +
|
|
tmp + ":" +
|
|
TQString::number( user->getUID() ) + ":" +
|
|
TQString::number( user->getGID() ) + ":" +
|
|
user->getClass() + ":" +
|
|
TQString::number( user->getLastChange() ) + ":" +
|
|
TQString::number( user->getExpire() ) + ":";
|
|
|
|
s1 =
|
|
user->getFullName() + "," +
|
|
user->getOffice() + "," +
|
|
user->getWorkPhone() + "," +
|
|
user->getHomePhone();
|
|
#else
|
|
escstr( getOffice1, setOffice1 );
|
|
escstr( getOffice2, setOffice2 );
|
|
escstr( getAddress, setAddress );
|
|
s =
|
|
user->getName() + ":" +
|
|
tmp + ":" +
|
|
TQString::number( user->getUID() ) + ":" +
|
|
TQString::number( user->getGID() ) + ":";
|
|
|
|
s1 =
|
|
user->getFullName() + "," +
|
|
user->getOffice1() + "," +
|
|
user->getOffice2() + "," +
|
|
user->getAddress();
|
|
|
|
#endif
|
|
for (int j=(s1.length()-1); j>=0; j--) {
|
|
if (s1[j] != ',')
|
|
break;
|
|
s1.truncate(j);
|
|
}
|
|
|
|
s += s1 + ":" +
|
|
user->getHomeDir() + ":" +
|
|
user->getShell() + "\n";
|
|
|
|
if( (nispasswd_fd != 0) && (minuid != 0) ) {
|
|
if (minuid <= tmp_uid) {
|
|
fputs(s.local8Bit().data(), nispasswd_fd);
|
|
nis_users_written++;
|
|
++it;
|
|
user = (*it);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if( (nispasswd_fd != 0) && (minuid == 0) ) {
|
|
errors_found = errors_found | NOMINUID;
|
|
}
|
|
|
|
if( (nispasswd_fd == 0) && (minuid != 0) ) {
|
|
errors_found = errors_found | NONISPASSWD;
|
|
}
|
|
kdDebug() << s << endl;
|
|
fputs(s.local8Bit().data(), passwd_fd);
|
|
|
|
++it;
|
|
user = (*it);
|
|
}
|
|
|
|
if(passwd_fd) {
|
|
fclose(passwd_fd);
|
|
chmod(TQFile::encodeName(new_passwd_filename), pwd_mode);
|
|
chown(TQFile::encodeName(new_passwd_filename), pwd_uid, pwd_gid);
|
|
rename(TQFile::encodeName(new_passwd_filename),
|
|
TQFile::encodeName(passwd_filename));
|
|
}
|
|
|
|
if(nispasswd_fd) {
|
|
fclose(nispasswd_fd);
|
|
chmod(TQFile::encodeName(new_nispasswd_filename), pwd_mode);
|
|
chown(TQFile::encodeName(new_nispasswd_filename), pwd_uid, pwd_gid);
|
|
rename(TQFile::encodeName(new_nispasswd_filename),
|
|
TQFile::encodeName(nispasswd_filename));
|
|
}
|
|
|
|
if( (errors_found & NOMINUID) != 0 ) {
|
|
KMessageBox::error( 0, i18n("Unable to process NIS passwd file without a minimum UID specified.\nPlease update KUser settings (Files).") );
|
|
}
|
|
|
|
if( (errors_found & NONISPASSWD) != 0 ) {
|
|
KMessageBox::error( 0, i18n("Specifying NIS minimum UID requires NIS file(s).\nPlease update KUser settings (Files).") );
|
|
}
|
|
|
|
// need to run a utility program to build /etc/passwd, /etc/pwd.db
|
|
// and /etc/spwd.db from /etc/master.passwd
|
|
#if defined(__FreeBSD__) || defined(__bsdi__)
|
|
if (system(PWMKDB) != 0) {
|
|
KMessageBox::error( 0, i18n("Unable to build password database.") );
|
|
return FALSE;
|
|
}
|
|
#else
|
|
if( (nis_users_written > 0) || (nispasswd_filename == passwd_filename) ) {
|
|
if (system(PWMKDB) != 0) {
|
|
KMessageBox::error( 0, i18n("Unable to build password databases.") );
|
|
return FALSE;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
#undef escstr
|
|
|
|
// Save shadow passwords file
|
|
|
|
bool KUserFiles::savesdw()
|
|
{
|
|
#ifdef HAVE_SHADOW
|
|
bool addok = false;
|
|
TQString tmp;
|
|
FILE *f;
|
|
struct spwd *spwp;
|
|
struct spwd s;
|
|
KU::KUser *up;
|
|
TQString shadow_file = mCfg->shadowsrc();
|
|
TQString new_shadow_file = shadow_file+TQString::fromLatin1(KU_CREATE_EXT);
|
|
|
|
if ( shadow_file.isEmpty() )
|
|
return TRUE;
|
|
|
|
if (!s_backuped) {
|
|
if (!backup(shadow_file)) return FALSE;
|
|
s_backuped = TRUE;
|
|
}
|
|
|
|
if ((f = fopen(TQFile::encodeName(new_shadow_file), "w")) == NULL) {
|
|
KMessageBox::error( 0, i18n("Error opening %1 for writing.").arg(new_shadow_file) );
|
|
return FALSE;
|
|
}
|
|
|
|
s.sp_namp = (char *)malloc(200);
|
|
s.sp_pwdp = (char *)malloc(200);
|
|
|
|
TQPtrListIterator<KU::KUser> it( mUsers );
|
|
up = (*it);
|
|
while (true) {
|
|
|
|
if ( up == 0 ) {
|
|
if ( addok ) break;
|
|
it = TQPtrListIterator<KU::KUser> ( mAdd );
|
|
up = (*it);
|
|
addok = true;
|
|
if ( up == 0 ) break;
|
|
};
|
|
|
|
if ( mDel.containsRef( up ) ) {
|
|
++it;
|
|
up = (*it);
|
|
continue;
|
|
}
|
|
if ( mMod.contains( up ) ) up = &( mMod[ up ] );
|
|
|
|
strncpy( s.sp_namp, up->getName().local8Bit(), 200 );
|
|
if ( up->getDisabled() )
|
|
strncpy( s.sp_pwdp, TQString("!!" + up->getSPwd()).local8Bit(), 200 );
|
|
else
|
|
strncpy( s.sp_pwdp, up->getSPwd().local8Bit(), 200 );
|
|
|
|
s.sp_lstchg = timeToDays( up->getLastChange() );
|
|
s.sp_min = up->getMin();
|
|
s.sp_max = up->getMax();
|
|
#ifndef _SCO_DS
|
|
s.sp_warn = up->getWarn();
|
|
s.sp_inact = up->getInactive();
|
|
s.sp_expire = timeToDays( up->getExpire() );
|
|
s.sp_flag = up->getFlag();
|
|
#endif
|
|
spwp = &s;
|
|
putspent(spwp, f);
|
|
|
|
++it;
|
|
up = (*it);
|
|
}
|
|
fclose(f);
|
|
|
|
chmod(TQFile::encodeName(new_shadow_file), sdw_mode);
|
|
chown(TQFile::encodeName(new_shadow_file), sdw_uid, sdw_gid);
|
|
rename(TQFile::encodeName(new_shadow_file),
|
|
TQFile::encodeName(shadow_file));
|
|
|
|
free(s.sp_namp);
|
|
free(s.sp_pwdp);
|
|
#endif // HAVE_SHADOW
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
void KUserFiles::createPassword( KU::KUser *user, const TQString &password )
|
|
{
|
|
if ( caps & Cap_Shadow ) {
|
|
user->setSPwd( encryptPass( password, mCfg->md5shadow() ) );
|
|
user->setPwd( TQString::fromLatin1("x") );
|
|
} else
|
|
user->setPwd( encryptPass( password, false ) );
|
|
}
|
|
|
|
bool KUserFiles::dbcommit()
|
|
{
|
|
bool ret;
|
|
mode_t mode;
|
|
|
|
mAddSucc.clear();
|
|
mDelSucc.clear();
|
|
mModSucc.clear();
|
|
if ( mDel.isEmpty() && mAdd.isEmpty() && mMod.isEmpty() )
|
|
return true;
|
|
|
|
mode = umask(0077);
|
|
ret = savepwd();
|
|
if ( ret && ( caps & Cap_Shadow ) ) ret = savesdw();
|
|
umask( mode );
|
|
if ( !ret ) return false;
|
|
|
|
mDelSucc = mDel;
|
|
mAddSucc = mAdd;
|
|
mModSucc = mMod;
|
|
mDel.clear();
|
|
mAdd.clear();
|
|
mMod.clear();
|
|
return TRUE;
|
|
}
|