/*
 *
 * $Id: k3bsetup2.cpp 623771 2007-01-15 13:47:39Z trueg $
 * Copyright (C) 2003-2007 Sebastian Trueg <trueg@k3b.org>
 *
 * This file is part of the K3b project.
 * Copyright (C) 1998-2007 Sebastian Trueg <trueg@k3b.org>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * See the file "COPYING" for the exact licensing terms.
 */

#include <config.h>

#include <tqlayout.h>
#include <tqmap.h>
#include <tqfile.h>
#include <tqfileinfo.h>
#include <tqcheckbox.h>
#include <tqlineedit.h>
#include <tqlabel.h>
#include <tqpushbutton.h>
#include <tqscrollview.h>
#include <tqtimer.h>

#include <tdelocale.h>
#include <tdeglobal.h>
#include <kgenericfactory.h>
#include <tdelistview.h>
#include <keditlistbox.h>
#include <tdemessagebox.h>
#include <kinputdialog.h>
#include <tdestandarddirs.h>
#include <tdeconfig.h>
#include <tdeversion.h>
#include <ktextedit.h>

#include "k3bsetup2.h"
#include "base_k3bsetup2.h"

#include <k3bdevicemanager.h>
#include <k3bdevice.h>
#include <k3bexternalbinmanager.h>
#include <k3bdefaultexternalprograms.h>
#include <k3bglobals.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <grp.h>



static bool shouldRunSuidRoot( K3bExternalBin* bin )
{
  //
  // Since kernel 2.6.8 older cdrecord versions are not able to use the SCSI subsystem when running suid root anymore
  // So for we ignore the suid root issue with kernel >= 2.6.8 and cdrecord < 2.01.01a02
  //
  // Some kernel version 2.6.16.something again introduced a problem here. Since I do not know the exact version
  // and a workaround was introduced in cdrecord 2.01.01a05 just use that version as the first for suid root.
  //
  // Seems as if cdrdao never had problems with suid root...
  //
  
  if( bin->name() == "cdrecord" ) {
    return ( K3b::simpleKernelVersion() < K3bVersion( 2, 6, 8 ) ||
	     bin->version >= K3bVersion( 2, 1, 1, "a05" ) ||
	     bin->hasFeature( "wodim" ) );
  }
  else if( bin->name() == "cdrdao" ) {
    return true;
  }
  else if( bin->name() == "growisofs" ) {
    //
    // starting with 6.0 growiofs raises it's priority using nice(-20)
    // BUT: newer kernels have ridiculously low default memorylocked resource limit, which prevents privileged 
    // users from starting growisofs 6.0 with "unable to anonymously mmap 33554432: Resource temporarily unavailable" 
    // error message. Until Andy releases a version including a workaround we simply never configure growisofs suid root
    return false; // bin->version >= K3bVersion( 6, 0 );
  }
  else
    return false;
}


class K3bSetup2::Private
{
public:
  K3bDevice::DeviceManager* deviceManager;
  K3bExternalBinManager* externalBinManager;

  bool changesNeeded;

  TQMap<TQCheckListItem*, TQString> listDeviceMap;
  TQMap<TQString, TQCheckListItem*> deviceListMap;

  TQMap<TQCheckListItem*, K3bExternalBin*> listBinMap;
  TQMap<K3bExternalBin*, TQCheckListItem*> binListMap;

  TDEConfig* config;
};



K3bSetup2::K3bSetup2( TQWidget *parent, const char *, const TQStringList& )
  : TDECModule( parent, "k3bsetup" )
{
  d = new Private();
  d->config = new TDEConfig( "k3bsetup2rc" );

  m_aboutData = new TDEAboutData("k3bsetup2",
			       "K3bSetup 2",
			       0, 0, TDEAboutData::License_GPL,
			       "(C) 2003-2007 Sebastian Trueg");
  m_aboutData->addAuthor("Sebastian Trueg", 0, "trueg@k3b.org");

  setButtons( TDECModule::Apply|TDECModule::Cancel|TDECModule::Ok|TDECModule::Default|TDECModule::Help );

  TQHBoxLayout* box = new TQHBoxLayout( this );
  box->setAutoAdd(true);
  box->setMargin(0);
  box->setSpacing( KDialog::spacingHint() );

  KTextEdit* label = new KTextEdit( this );
  label->setText( "<h2>K3bSetup</h2>"
		  + i18n("<p>This simple setup assistant is able to set the permissions needed by K3b in order to "
			 "burn CDs and DVDs. "
			 "<p>It does not take things like devfs or resmgr into account. In most cases this is not a "
			 "problem but on some systems the permissions may be altered the next time you login or restart "
			 "your computer. In those cases it is best to consult the distribution documentation."
			 "<p><b>Caution:</b> Although K3bSetup 2 should not be able "
			 "to mess up your system no guarantee can be given.") );
  label->setReadOnly( true );
  label->setFixedWidth( 200 );

  w = new base_K3bSetup2( this );

  // TODO: enable this and let root specify users
  w->m_editUsers->hide();
  w->textLabel2->hide();


  connect( w->m_checkUseBurningGroup, TQ_SIGNAL(toggled(bool)),
	   this, TQ_SLOT(updateViews()) );
  connect( w->m_editBurningGroup, TQ_SIGNAL(textChanged(const TQString&)),
	   this, TQ_SLOT(updateViews()) );
  connect( w->m_editSearchPath, TQ_SIGNAL(changed()),
	   this, TQ_SLOT(slotSearchPrograms()) );
  connect( w->m_buttonAddDevice, TQ_SIGNAL(clicked()),
	   this, TQ_SLOT(slotAddDevice()) );


  d->externalBinManager = new K3bExternalBinManager( this );
  d->deviceManager = new K3bDevice::DeviceManager( this );

  // these are the only programs that need special permissions
  d->externalBinManager->addProgram( new K3bCdrdaoProgram() );
  d->externalBinManager->addProgram( new K3bCdrecordProgram(false) );
  d->externalBinManager->addProgram( new K3bGrowisofsProgram() );

  d->externalBinManager->search();
  d->deviceManager->scanBus();

  load();

  //
  // This is a hack to work around a kcm bug which makes the faulty assumption that
  // every module starts without anything to apply
  //
  TQTimer::singleShot( 0, this, TQ_SLOT(updateViews()) );

  if( getuid() != 0 || !d->config->checkConfigFilesWritable( true ) )
    makeReadOnly();
}


K3bSetup2::~K3bSetup2()
{
  delete d->config;
  delete d;
  delete m_aboutData;
}


void K3bSetup2::updateViews()
{
  d->changesNeeded = false;

  updatePrograms();
  updateDevices();

  emit changed( ( getuid() != 0 ) ? false : d->changesNeeded );
}


void K3bSetup2::updatePrograms()
{
  // first save which were checked
  TQMap<K3bExternalBin*, bool> checkMap;
  TQListViewItemIterator listIt( w->m_viewPrograms );
  for( ; listIt.current(); ++listIt )
    checkMap.insert( d->listBinMap[(TQCheckListItem*)*listIt], ((TQCheckListItem*)*listIt)->isOn() );

  w->m_viewPrograms->clear();
  d->binListMap.clear();
  d->listBinMap.clear();

  // load programs
  const TQMap<TQString, K3bExternalProgram*>& map = d->externalBinManager->programs();
  for( TQMap<TQString, K3bExternalProgram*>::const_iterator it = map.begin(); it != map.end(); ++it ) {
    K3bExternalProgram* p = it.data();

    TQPtrListIterator<K3bExternalBin> binIt( p->bins() );
    for( ; binIt.current(); ++binIt ) {
      K3bExternalBin* b = *binIt;

      TQFileInfo fi( b->path );
      // we need the uid bit which is not supported by TQFileInfo
      struct stat s;
      if( ::stat( TQFile::encodeName(b->path), &s ) ) {
	kdDebug() << "(K3bSetup2) unable to stat " << b->path << endl;
      }
      else {
	// create a checkviewitem
	TQCheckListItem* bi = new TQCheckListItem( w->m_viewPrograms, b->name(), TQCheckListItem::CheckBox );
	bi->setText( 1, b->version );
	bi->setText( 2, b->path );

	d->listBinMap.insert( bi, b );
	d->binListMap.insert( b, bi );

	// check the item on first insertion or if it was checked before
	bi->setOn( checkMap.contains(b) ? checkMap[b] : true );

	int perm = s.st_mode & 0007777;

	TQString wantedGroup("root");
	if( w->m_checkUseBurningGroup->isChecked() )
	  wantedGroup = burningGroup();

	int wantedPerm = 0;
	if( shouldRunSuidRoot( b ) ) {
	  if( w->m_checkUseBurningGroup->isChecked() )
	    wantedPerm = 0004710;
	  else
	    wantedPerm = 0004711;
	}
	else {
	  if( w->m_checkUseBurningGroup->isChecked() )
	    wantedPerm = 0000750;
	  else
	    wantedPerm = 0000755;
	}

	bi->setText( 3, TQString::number( perm, 8 ).rightJustify( 4, '0' ) + " " + fi.owner() + "." + fi.group() );
	if( perm != wantedPerm ||
	    fi.owner() != "root" ||
	    fi.group() != wantedGroup ) {
	  bi->setText( 4, TQString("%1 root.%2").arg(wantedPerm,0,8).arg(wantedGroup) );
	  if( bi->isOn() )
	    d->changesNeeded = true;
	}
	else
	  bi->setText( 4, i18n("no change") );
      }
    }
  }
}


void K3bSetup2::updateDevices()
{
  // first save which were checked
  TQMap<TQString, bool> checkMap;
  TQListViewItemIterator listIt( w->m_viewDevices );
  for( ; listIt.current(); ++listIt )
    checkMap.insert( d->listDeviceMap[(TQCheckListItem*)*listIt], ((TQCheckListItem*)*listIt)->isOn() );

  w->m_viewDevices->clear();
  d->listDeviceMap.clear();
  d->deviceListMap.clear();

  TQPtrListIterator<K3bDevice::Device> it( d->deviceManager->allDevices() );
  for( ; it.current(); ++it ) {
    K3bDevice::Device* device = *it;
    // check the item on first insertion or if it was checked before
    TQCheckListItem* item = createDeviceItem( device->blockDeviceName() );
    item->setOn( checkMap.contains(device->blockDeviceName()) ? checkMap[device->blockDeviceName()] : true );
    item->setText( 0, device->vendor() + " " + device->description() );
  
    if( !device->genericDevice().isEmpty() ) {
      TQCheckListItem* item = createDeviceItem( device->genericDevice() );
      item->setOn( checkMap.contains(device->genericDevice()) ? checkMap[device->genericDevice()] : true );
      item->setText( 0, device->vendor() + " " + device->description() + " (" + i18n("Generic SCSI Device") + ")" );
    }
  }
}


TQCheckListItem* K3bSetup2::createDeviceItem( const TQString& deviceNode )
{
  TQFileInfo fi( deviceNode );
  struct stat s;
  if( ::stat( TQFile::encodeName(deviceNode), &s ) ) {
    kdDebug() << "(K3bSetup2) unable to stat " << deviceNode << endl;
    return 0;
  }
  else {
    // create a checkviewitem
    TQCheckListItem* bi = new TQCheckListItem( w->m_viewDevices,
					     deviceNode,
					     TQCheckListItem::CheckBox );

    d->listDeviceMap.insert( bi, deviceNode );
    d->deviceListMap.insert( deviceNode, bi );

    bi->setText( 1, deviceNode );

    int perm = s.st_mode & 0000777;

    bi->setText( 2, TQString::number( perm, 8 ).rightJustify( 3, '0' ) + " " + fi.owner() + "." + fi.group() );
    if( w->m_checkUseBurningGroup->isChecked() ) {
      // we ignore the device's owner here
      if( perm != 0000660 ||
	  fi.group() != burningGroup() ) {
	bi->setText( 3, "660 " + fi.owner() + "." + burningGroup() );
	if( bi->isOn() )
	  d->changesNeeded = true;
      }
      else
	bi->setText( 3, i18n("no change") );
    }
    else {
      // we ignore the device's owner and group here
      if( perm != 0000666 ) {
	bi->setText( 3, "666 " + fi.owner() + "." + fi.group()  );
	if( bi->isOn() )
	  d->changesNeeded = true;
      }
      else
	bi->setText( 3, i18n("no change") );
    }

    return bi;
  }
}


void K3bSetup2::load()
{
  if( d->config->hasGroup("External Programs") ) {
    d->config->setGroup( "External Programs" );
    d->externalBinManager->readConfig( d->config );
  }
  if( d->config->hasGroup("Devices") ) {
    d->config->setGroup( "Devices" );
    d->deviceManager->readConfig( d->config );
  }

  d->config->setGroup( "General Settings" );
  w->m_checkUseBurningGroup->setChecked( d->config->readBoolEntry( "use burning group", false ) );
  w->m_editBurningGroup->setText( d->config->readEntry( "burning group", "burning" ) );


  // load search path
  w->m_editSearchPath->clear();
  w->m_editSearchPath->insertStringList( d->externalBinManager->searchPath() );

  updateViews();
}


void K3bSetup2::defaults()
{
  w->m_checkUseBurningGroup->setChecked(false);
  w->m_editBurningGroup->setText( "burning" );

  //
  // This is a hack to work around a kcm bug which makes the faulty assumption that
  // every module defaults to a state where nothing is to be applied
  //
  TQTimer::singleShot( 0, this, TQ_SLOT(updateViews()) );
}


void K3bSetup2::save()
{
  d->config->setGroup( "General Settings" );
  d->config->writeEntry( "use burning group", w->m_checkUseBurningGroup->isChecked() );
  d->config->writeEntry( "burning group", burningGroup() );
  d->config->setGroup( "External Programs");
  d->externalBinManager->saveConfig( d->config );
  d->config->setGroup( "Devices");
  d->deviceManager->saveConfig( d->config );


  bool success = true;

  struct group* g = 0;
  if( w->m_checkUseBurningGroup->isChecked() ) {
    // TODO: create the group if it's not there
    g = getgrnam( burningGroup().local8Bit() );
    if( !g ) {
      KMessageBox::error( this, i18n("There is no group %1.").arg(burningGroup()) );
      return;
    }
  }


  // save the device permissions
  TQListViewItemIterator listIt( w->m_viewDevices );
  for( ; listIt.current(); ++listIt ) {

    TQCheckListItem* checkItem = (TQCheckListItem*)listIt.current();

    if( checkItem->isOn() ) {
      TQString dev = d->listDeviceMap[checkItem];

      if( w->m_checkUseBurningGroup->isChecked() ) {
	if( ::chmod( TQFile::encodeName(dev), S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP ) )
	  success = false;

	if( ::chown( TQFile::encodeName(dev), (gid_t)-1, g->gr_gid ) )
	  success = false;
      }
      else {
	if( ::chmod( TQFile::encodeName(dev), S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH ) )
	  success = false;
      }
    }
  }


  // save the program permissions
  listIt = TQListViewItemIterator( w->m_viewPrograms );
  for( ; listIt.current(); ++listIt ) {

    TQCheckListItem* checkItem = (TQCheckListItem*)listIt.current();

    if( checkItem->isOn() ) {

      K3bExternalBin* bin = d->listBinMap[checkItem];

      if( w->m_checkUseBurningGroup->isChecked() ) {
	if( ::chown( TQFile::encodeName(bin->path), (gid_t)0, g->gr_gid ) )
	  success = false;

	int perm = 0;
	if( shouldRunSuidRoot( bin ) )
	  perm = S_ISUID|S_IRWXU|S_IXGRP;
	else
	  perm = S_IRWXU|S_IXGRP|S_IRGRP;

	if( ::chmod( TQFile::encodeName(bin->path), perm ) )
	  success = false;
      }
      else {
	if( ::chown( TQFile::encodeName(bin->path), 0, 0 ) )
	  success = false;

	int perm = 0;
	if( shouldRunSuidRoot( bin ) )
	  perm = S_ISUID|S_IRWXU|S_IXGRP|S_IXOTH;
	else
	  perm = S_IRWXU|S_IXGRP|S_IRGRP|S_IXOTH|S_IROTH;

	if( ::chmod( TQFile::encodeName(bin->path), perm ) )
	  success = false;
      }
    }
  }


  if( success )
    KMessageBox::information( this, i18n("Successfully updated all permissions.") );
  else {
    if( getuid() )
      KMessageBox::error( this, i18n("Could not update all permissions. You should run K3bSetup 2 as root.") );
    else
      KMessageBox::error( this, i18n("Could not update all permissions.") );
  }

  // WE MAY USE "newgrp -" to reinitialize the environment if we add users to a group

  updateViews();
}


TQString K3bSetup2::quickHelp() const
{
  return i18n("<h2>K3bSetup 2</h2>"
	      "<p>This simple setup assistant is able to set the permissions needed by K3b in order to "
	      "burn CDs and DVDs."
	      "<p>It does not take into account devfs or resmgr, or similar. In most cases this is not a "
	      "problem, but on some systems the permissions may be altered the next time you login or restart "
	      "your computer. In these cases it is best to consult the distribution's documentation."
	      "<p>The important task that K3bSetup 2 performs is grant write access to the CD and DVD devices."
	      "<p><b>Caution:</b> Although K3bSetup 2 should not be able "
	      "to damage your system, no guarantee can be given.");
}


TQString K3bSetup2::burningGroup() const
{
  TQString g = w->m_editBurningGroup->text();
  return g.isEmpty() ? TQString("burning") : g;
}


void K3bSetup2::slotSearchPrograms()
{
  d->externalBinManager->setSearchPath( w->m_editSearchPath->items() );
  d->externalBinManager->search();
  updatePrograms();

  emit changed( d->changesNeeded );
}


void K3bSetup2::slotAddDevice()
{
  bool ok;
  TQString newDevicename = KInputDialog::getText( i18n("Location of New Drive"),
                                                 i18n("Please enter the device name where K3b should search\n"
						 "for a new drive (example: /dev/mebecdrom):"),
						 "/dev/", &ok, this );

  if( ok ) {
    if( d->deviceManager->addDevice( newDevicename ) ) {
      updateDevices();

      emit changed( d->changesNeeded );
    }
    else
      KMessageBox::error( this, i18n("Could not find an additional device at\n%1").arg(newDevicename),
			  i18n("Error"), false );
  }
}

void K3bSetup2::makeReadOnly()
{
  w->m_checkUseBurningGroup->setEnabled( false );
  w->m_editBurningGroup->setEnabled( false );
  w->m_editUsers->setEnabled( false );
  w->m_viewDevices->setEnabled( false );
  w->m_buttonAddDevice->setEnabled( false );
  w->m_viewPrograms->setEnabled( false );
  w->m_editSearchPath->setEnabled( false );
}


typedef KGenericFactory<K3bSetup2, TQWidget> K3bSetup2Factory;
K_EXPORT_COMPONENT_FACTORY( kcm_k3bsetup2, K3bSetup2Factory("k3bsetup") )


#include "k3bsetup2.moc"
