/* This file is part of the KDE libraries
    Copyright (C) 2000 Stephan Kulow <coolo@kde.org>
                       David Faure <faure@kde.org>
                       Waldo Bastian <bastian@kde.org>

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Library General Public
    License as published by the Free Software Foundation; either
    version 2 of the License, or (at your option) any later version.

    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 "chmodjob.h"

#include "job.h"
#include "jobuidelegate.h"

#include <kglobal.h>
#include <kuiserverjobtracker.h>

#include <klocale.h>
#include <kdebug.h>
#include <kmessagebox.h>
#include <QtCore/QFile>

#include <config.h>

#include <pwd.h>
#include <grp.h>
#include <sys/types.h>
#include <unistd.h>
#include <assert.h>

#include "job_p.h"

namespace KIO {

    struct ChmodInfo
    {
        KUrl url;
        int permissions;
    };

    enum ChmodJobState {
        CHMODJOB_STATE_LISTING,
        CHMODJOB_STATE_CHMODING
    };

    class ChmodJobPrivate: public KIO::JobPrivate
    {
    public:
        ChmodJobPrivate(const KFileItemList& lstItems, int permissions, int mask,
                        int newOwner, int newGroup, bool recursive)
            : state( CHMODJOB_STATE_LISTING )
            , m_permissions( permissions )
            , m_mask( mask )
            , m_newOwner( newOwner )
            , m_newGroup( newGroup )
            , m_recursive( recursive )
            , m_lstItems( lstItems )
        {
        }

        ChmodJobState state;
        int m_permissions;
        int m_mask;
        int m_newOwner;
        int m_newGroup;
        bool m_recursive;
        KFileItemList m_lstItems;
        QLinkedList<ChmodInfo> m_infos; // linkedlist since we keep removing the first item

        void chmodNextFile();
        void _k_slotEntries( KIO::Job * , const KIO::UDSEntryList & );
        void _k_processList();

        Q_DECLARE_PUBLIC(ChmodJob)

        static inline ChmodJob *newJob(const KFileItemList& lstItems, int permissions, int mask,
                                       int newOwner, int newGroup, bool recursive, JobFlags flags)
        {
            ChmodJob *job = new ChmodJob(*new ChmodJobPrivate(lstItems,permissions,mask,
                                                              newOwner,newGroup,recursive));
            job->setUiDelegate(new JobUiDelegate());
            if (!(flags & HideProgressInfo))
                KIO::getJobTracker()->registerJob(job);
            return job;
        }
    };

} // namespace KIO

using namespace KIO;

ChmodJob::ChmodJob(ChmodJobPrivate &dd)
    : KIO::Job(dd)
{
    QMetaObject::invokeMethod( this, "_k_processList", Qt::QueuedConnection );
}

ChmodJob::~ChmodJob()
{
}

void ChmodJobPrivate::_k_processList()
{
    Q_Q(ChmodJob);
    while ( !m_lstItems.isEmpty() )
    {
        const KFileItem item = m_lstItems.first();
        if ( !item.isLink() ) // don't do anything with symlinks
        {
            // File or directory -> remember to chmod
            ChmodInfo info;
            info.url = item.url();
            // This is a toplevel file, we apply changes directly (no +X emulation here)
            info.permissions = ( m_permissions & m_mask ) | ( item.permissions() & ~m_mask );
            /*kDebug(7007) << "\n current permissions=" << QString::number(item->permissions(),8)
                          << "\n wanted permission=" << QString::number(m_permissions,8)
                          << "\n with mask=" << QString::number(m_mask,8)
                          << "\n with ~mask (mask bits we keep) =" << QString::number((uint)~m_mask,8)
                          << "\n bits we keep =" << QString::number(item->permissions() & ~m_mask,8)
                          << "\n new permissions = " << QString::number(info.permissions,8);*/
            m_infos.prepend( info );
            //kDebug(7007) << "processList : Adding info for " << info.url;
            // Directory and recursive -> list
            if ( item.isDir() && m_recursive )
            {
                //kDebug(7007) << "ChmodJob::processList dir -> listing";
                KIO::ListJob * listJob = KIO::listRecursive( item.url(), KIO::HideProgressInfo );
                q->connect( listJob, SIGNAL(entries( KIO::Job *,
                                                     const KIO::UDSEntryList& )),
                            SLOT(_k_slotEntries( KIO::Job*, const KIO::UDSEntryList& )));
                q->addSubjob( listJob );
                return; // we'll come back later, when this one's finished
            }
        }
        m_lstItems.removeFirst();
    }
    kDebug(7007) << "ChmodJob::processList -> going to STATE_CHMODING";
    // We have finished, move on
    state = CHMODJOB_STATE_CHMODING;
    chmodNextFile();
}

void ChmodJobPrivate::_k_slotEntries( KIO::Job*, const KIO::UDSEntryList & list )
{
    KIO::UDSEntryList::ConstIterator it = list.begin();
    KIO::UDSEntryList::ConstIterator end = list.end();
    for (; it != end; ++it) {
        const KIO::UDSEntry& entry = *it;
        const bool isLink = !entry.stringValue( KIO::UDSEntry::UDS_STRING ).isEmpty();
        const QString relativePath = entry.stringValue( KIO::UDSEntry::UDS_NAME );
        if ( !isLink && relativePath != ".." )
        {
            const mode_t permissions = entry.numberValue( KIO::UDSEntry::UDS_ACCESS );

            ChmodInfo info;
            info.url = m_lstItems.first().url(); // base directory
            info.url.addPath( relativePath );
            int mask = m_mask;
            // Emulate -X: only give +x to files that had a +x bit already
            // So the check is the opposite : if the file had no x bit, don't touch x bits
            // For dirs this doesn't apply
            if ( !entry.isDir() )
            {
                int newPerms = m_permissions & mask;
                if ( (newPerms & 0111) && !(permissions & 0111) )
                {
                    // don't interfere with mandatory file locking
                    if ( newPerms & 02000 )
                      mask = mask & ~0101;
                    else
                      mask = mask & ~0111;
                }
            }
            info.permissions = ( m_permissions & mask ) | ( permissions & ~mask );
            /*kDebug(7007) << "\n current permissions=" << QString::number(permissions,8)
                          << "\n wanted permission=" << QString::number(m_permissions,8)
                          << "\n with mask=" << QString::number(mask,8)
                          << "\n with ~mask (mask bits we keep) =" << QString::number((uint)~mask,8)
                          << "\n bits we keep =" << QString::number(permissions & ~mask,8)
                          << "\n new permissions = " << QString::number(info.permissions,8);*/
            // Prepend this info in our todo list.
            // This way, the toplevel dirs are done last.
            m_infos.prepend( info );
        }
    }
}

void ChmodJobPrivate::chmodNextFile()
{
    Q_Q(ChmodJob);
    if ( !m_infos.isEmpty() )
    {
        ChmodInfo info = m_infos.takeFirst();
        // First update group / owner (if local file)
        // (permissions have to set after, in case of suid and sgid)
        if ( info.url.isLocalFile() && ( m_newOwner != -1 || m_newGroup != -1 ) )
        {
            QString path = info.url.path();
            if ( chown( QFile::encodeName(path), m_newOwner, m_newGroup ) != 0 )
            {
                int answer = KMessageBox::warningContinueCancel( 0, i18n( "<qt>Could not modify the ownership of file <b>%1</b>. You have insufficient access to the file to perform the change.</qt>" , path), QString(), KGuiItem(i18n("&Skip File")) );
                if (answer == KMessageBox::Cancel)
                {
                    q->setError( ERR_USER_CANCELED );
                    q->emitResult();
                    return;
                }
            }
        }

        kDebug(7007) << "chmod'ing" << info.url
                      << "to" << QString::number(info.permissions,8);
        KIO::SimpleJob * job = KIO::chmod( info.url, info.permissions );
        // copy the metadata for acl and default acl
        const QString aclString = q->queryMetaData( QLatin1String("ACL_STRING") );
        const QString defaultAclString = q->queryMetaData( QLatin1String("DEFAULT_ACL_STRING") );
        if ( !aclString.isEmpty() )
            job->addMetaData( QLatin1String("ACL_STRING"), aclString );
        if ( !defaultAclString.isEmpty() )
            job->addMetaData( QLatin1String("DEFAULT_ACL_STRING"), defaultAclString );
        q->addSubjob(job);
    }
    else
        // We have finished
        q->emitResult();
}

void ChmodJob::slotResult( KJob * job )
{
    Q_D(ChmodJob);
    removeSubjob(job);
    if ( job->error() )
    {
        setError( job->error() );
        setErrorText( job->errorText() );
        emitResult();
        return;
    }
    //kDebug(7007) << "d->m_lstItems:" << d->m_lstItems.count();
    switch ( d->state )
    {
        case CHMODJOB_STATE_LISTING:
            d->m_lstItems.removeFirst();
            kDebug(7007) << "-> processList";
            d->_k_processList();
            return;
        case CHMODJOB_STATE_CHMODING:
            kDebug(7007) << "-> chmodNextFile";
            d->chmodNextFile();
            return;
        default:
            assert(0);
            return;
    }
}

ChmodJob *KIO::chmod( const KFileItemList& lstItems, int permissions, int mask,
                      const QString& owner, const QString& group,
                      bool recursive, JobFlags flags )
{
    uid_t newOwnerID = uid_t(-1); // chown(2) : -1 means no change
    if ( !owner.isEmpty() )
    {
        struct passwd* pw = getpwnam(QFile::encodeName(owner));
        if ( pw == 0L )
            kError(250) << " ERROR: No user" << owner;
        else
            newOwnerID = pw->pw_uid;
    }
    gid_t newGroupID = gid_t(-1); // chown(2) : -1 means no change
    if ( !group.isEmpty() )
    {
        struct group* g = getgrnam(QFile::encodeName(group));
        if ( g == 0L )
            kError(250) << " ERROR: No group" << group;
        else
            newGroupID = g->gr_gid;
    }
    return ChmodJobPrivate::newJob(lstItems, permissions, mask, newOwnerID,
                                   newGroupID, recursive, flags);
}

#include "chmodjob.moc"
