File indexing completed on 2024-04-21 07:41:52

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2000 Stephan Kulow <coolo@kde.org>
0004     SPDX-FileCopyrightText: 2000 David Faure <faure@kde.org>
0005     SPDX-FileCopyrightText: 2000 Waldo Bastian <bastian@kde.org>
0006 
0007     SPDX-License-Identifier: LGPL-2.0-or-later
0008 */
0009 
0010 #include "chmodjob.h"
0011 #include "../utils_p.h"
0012 
0013 #include <KLocalizedString>
0014 #include <KUser>
0015 #include <QDebug>
0016 
0017 #include "askuseractioninterface.h"
0018 #include "job_p.h"
0019 #include "jobuidelegatefactory.h"
0020 #include "kioglobal_p.h"
0021 #include "listjob.h"
0022 
0023 #include <stack>
0024 
0025 namespace KIO
0026 {
0027 struct ChmodInfo {
0028     QUrl url;
0029     int permissions;
0030 };
0031 
0032 enum ChmodJobState {
0033     CHMODJOB_STATE_LISTING,
0034     CHMODJOB_STATE_CHMODING,
0035 };
0036 
0037 class ChmodJobPrivate : public KIO::JobPrivate
0038 {
0039 public:
0040     ChmodJobPrivate(const KFileItemList &lstItems, int permissions, int mask, KUserId newOwner, KGroupId newGroup, bool recursive)
0041         : state(CHMODJOB_STATE_LISTING)
0042         , m_permissions(permissions)
0043         , m_mask(mask)
0044         , m_newOwner(newOwner)
0045         , m_newGroup(newGroup)
0046         , m_recursive(recursive)
0047         , m_bAutoSkipFiles(false)
0048         , m_lstItems(lstItems)
0049     {
0050     }
0051 
0052     ChmodJobState state;
0053     int m_permissions;
0054     int m_mask;
0055     KUserId m_newOwner;
0056     KGroupId m_newGroup;
0057     bool m_recursive;
0058     bool m_bAutoSkipFiles;
0059     KFileItemList m_lstItems;
0060     std::stack<ChmodInfo> m_infos;
0061 
0062     void chmodNextFile();
0063     void slotEntries(KIO::Job *, const KIO::UDSEntryList &);
0064     void processList();
0065 
0066     Q_DECLARE_PUBLIC(ChmodJob)
0067 
0068     static inline ChmodJob *
0069     newJob(const KFileItemList &lstItems, int permissions, int mask, KUserId newOwner, KGroupId newGroup, bool recursive, JobFlags flags)
0070     {
0071         ChmodJob *job = new ChmodJob(*new ChmodJobPrivate(lstItems, permissions, mask, newOwner, newGroup, recursive));
0072         job->setUiDelegate(KIO::createDefaultJobUiDelegate());
0073         if (!(flags & HideProgressInfo)) {
0074             KIO::getJobTracker()->registerJob(job);
0075         }
0076         if (!(flags & NoPrivilegeExecution)) {
0077             job->d_func()->m_privilegeExecutionEnabled = true;
0078             job->d_func()->m_operationType = ChangeAttr;
0079         }
0080         return job;
0081     }
0082 };
0083 
0084 } // namespace KIO
0085 
0086 using namespace KIO;
0087 
0088 ChmodJob::ChmodJob(ChmodJobPrivate &dd)
0089     : KIO::Job(dd)
0090 {
0091     Q_D(ChmodJob);
0092     auto processFunc = [d]() {
0093         d->processList();
0094     };
0095     QMetaObject::invokeMethod(this, processFunc, Qt::QueuedConnection);
0096 }
0097 
0098 ChmodJob::~ChmodJob()
0099 {
0100 }
0101 
0102 void ChmodJobPrivate::processList()
0103 {
0104     Q_Q(ChmodJob);
0105     while (!m_lstItems.isEmpty()) {
0106         const KFileItem item = m_lstItems.first();
0107         if (!item.isLink()) { // don't do anything with symlinks
0108             // File or directory -> remember to chmod
0109             ChmodInfo info;
0110             info.url = item.url();
0111             // This is a toplevel file, we apply changes directly (no +X emulation here)
0112             const mode_t permissions = item.permissions() & 0777; // get rid of "set gid" and other special flags
0113             info.permissions = (m_permissions & m_mask) | (permissions & ~m_mask);
0114             /*//qDebug() << "toplevel url:" << info.url << "\n current permissions=" << QString::number(permissions,8)
0115                           << "\n wanted permission=" << QString::number(m_permissions,8)
0116                           << "\n with mask=" << QString::number(m_mask,8)
0117                           << "\n with ~mask (mask bits we keep) =" << QString::number((uint)~m_mask,8)
0118                           << "\n bits we keep =" << QString::number(permissions & ~m_mask,8)
0119                           << "\n new permissions = " << QString::number(info.permissions,8);*/
0120             m_infos.push(std::move(info));
0121             // qDebug() << "processList : Adding info for " << info.url;
0122             // Directory and recursive -> list
0123             if (item.isDir() && m_recursive) {
0124                 // qDebug() << "ChmodJob::processList dir -> listing";
0125                 KIO::ListJob *listJob = KIO::listRecursive(item.url(), KIO::HideProgressInfo);
0126                 q->connect(listJob, &KIO::ListJob::entries, q, [this](KIO::Job *job, const KIO::UDSEntryList &entries) {
0127                     slotEntries(job, entries);
0128                 });
0129                 q->addSubjob(listJob);
0130                 return; // we'll come back later, when this one's finished
0131             }
0132         }
0133         m_lstItems.removeFirst();
0134     }
0135     // qDebug() << "ChmodJob::processList -> going to STATE_CHMODING";
0136     // We have finished, move on
0137     state = CHMODJOB_STATE_CHMODING;
0138     chmodNextFile();
0139 }
0140 
0141 void ChmodJobPrivate::slotEntries(KIO::Job *, const KIO::UDSEntryList &list)
0142 {
0143     KIO::UDSEntryList::ConstIterator it = list.begin();
0144     KIO::UDSEntryList::ConstIterator end = list.end();
0145     for (; it != end; ++it) {
0146         const KIO::UDSEntry &entry = *it;
0147         const bool isLink = !entry.stringValue(KIO::UDSEntry::UDS_LINK_DEST).isEmpty();
0148         const QString relativePath = entry.stringValue(KIO::UDSEntry::UDS_NAME);
0149         if (!isLink && relativePath != QLatin1String("..")) {
0150             const mode_t permissions = entry.numberValue(KIO::UDSEntry::UDS_ACCESS) & 0777; // get rid of "set gid" and other special flags
0151 
0152             ChmodInfo info;
0153             info.url = m_lstItems.first().url(); // base directory
0154             info.url.setPath(Utils::concatPaths(info.url.path(), relativePath));
0155             int mask = m_mask;
0156             // Emulate -X: only give +x to files that had a +x bit already
0157             // So the check is the opposite : if the file had no x bit, don't touch x bits
0158             // For dirs this doesn't apply
0159             if (!entry.isDir()) {
0160                 int newPerms = m_permissions & mask;
0161                 if ((newPerms & 0111) && !(permissions & 0111)) {
0162                     // don't interfere with mandatory file locking
0163                     if (newPerms & 02000) {
0164                         mask = mask & ~0101;
0165                     } else {
0166                         mask = mask & ~0111;
0167                     }
0168                 }
0169             }
0170             info.permissions = (m_permissions & mask) | (permissions & ~mask);
0171             /*//qDebug() << info.url << "\n current permissions=" << QString::number(permissions,8)
0172                           << "\n wanted permission=" << QString::number(m_permissions,8)
0173                           << "\n with mask=" << QString::number(mask,8)
0174                           << "\n with ~mask (mask bits we keep) =" << QString::number((uint)~mask,8)
0175                           << "\n bits we keep =" << QString::number(permissions & ~mask,8)
0176                           << "\n new permissions = " << QString::number(info.permissions,8);*/
0177             // Push this info on top of the stack so it's handled first.
0178             // This way, the toplevel dirs are done last.
0179             m_infos.push(std::move(info));
0180         }
0181     }
0182 }
0183 
0184 void ChmodJobPrivate::chmodNextFile()
0185 {
0186     auto processNextFunc = [this]() {
0187         chmodNextFile();
0188     };
0189 
0190     Q_Q(ChmodJob);
0191     if (!m_infos.empty()) {
0192         ChmodInfo info = m_infos.top();
0193         m_infos.pop();
0194         // First update group / owner (if local file)
0195         // (permissions have to be set after, in case of suid and sgid)
0196         if (info.url.isLocalFile() && (m_newOwner.isValid() || m_newGroup.isValid())) {
0197             QString path = info.url.toLocalFile();
0198             if (!KIOPrivate::changeOwnership(path, m_newOwner, m_newGroup)) {
0199                 auto *askUserActionInterface = KIO::delegateExtension<AskUserActionInterface *>(q);
0200                 if (!askUserActionInterface) {
0201                     Q_EMIT q->warning(q, i18n("Could not modify the ownership of file %1", path));
0202                 } else if (!m_bAutoSkipFiles) {
0203                     SkipDialog_Options options;
0204                     if (m_infos.size() > 1) {
0205                         options |= SkipDialog_MultipleItems;
0206                     }
0207 
0208                     auto skipSignal = &AskUserActionInterface::askUserSkipResult;
0209                     q->connect(askUserActionInterface, skipSignal, q, [=](KIO::SkipDialog_Result result, KJob *parentJob) {
0210                         Q_ASSERT(q == parentJob);
0211                         q->disconnect(askUserActionInterface, skipSignal, q, nullptr);
0212 
0213                         switch (result) {
0214                         case Result_AutoSkip:
0215                             m_bAutoSkipFiles = true;
0216                             // fall through
0217                             Q_FALLTHROUGH();
0218                         case Result_Skip:
0219                             QMetaObject::invokeMethod(q, processNextFunc, Qt::QueuedConnection);
0220                             return;
0221                         case Result_Retry:
0222                             m_infos.push(std::move(info));
0223                             QMetaObject::invokeMethod(q, processNextFunc, Qt::QueuedConnection);
0224                             return;
0225                         case Result_Cancel:
0226                         default:
0227                             q->setError(ERR_USER_CANCELED);
0228                             q->emitResult();
0229                             return;
0230                         }
0231                     });
0232 
0233                     askUserActionInterface->askUserSkip(q,
0234                                                         options,
0235                                                         xi18n("Could not modify the ownership of file <filename>%1</filename>. You have "
0236                                                               "insufficient access to the file to perform the change.",
0237                                                               path));
0238                     return;
0239                 }
0240             }
0241         }
0242 
0243         /*qDebug() << "chmod'ing" << info.url << "to" << QString::number(info.permissions,8);*/
0244         KIO::SimpleJob *job = KIO::chmod(info.url, info.permissions);
0245         job->setParentJob(q);
0246         // copy the metadata for acl and default acl
0247         const QString aclString = q->queryMetaData(QStringLiteral("ACL_STRING"));
0248         const QString defaultAclString = q->queryMetaData(QStringLiteral("DEFAULT_ACL_STRING"));
0249         if (!aclString.isEmpty()) {
0250             job->addMetaData(QStringLiteral("ACL_STRING"), aclString);
0251         }
0252         if (!defaultAclString.isEmpty()) {
0253             job->addMetaData(QStringLiteral("DEFAULT_ACL_STRING"), defaultAclString);
0254         }
0255         q->addSubjob(job);
0256     } else { // We have finished
0257         q->emitResult();
0258     }
0259 }
0260 
0261 void ChmodJob::slotResult(KJob *job)
0262 {
0263     Q_D(ChmodJob);
0264     removeSubjob(job);
0265     if (job->error()) {
0266         setError(job->error());
0267         setErrorText(job->errorText());
0268         emitResult();
0269         return;
0270     }
0271     // qDebug() << "d->m_lstItems:" << d->m_lstItems.count();
0272     switch (d->state) {
0273     case CHMODJOB_STATE_LISTING:
0274         d->m_lstItems.removeFirst();
0275         // qDebug() << "-> processList";
0276         d->processList();
0277         return;
0278     case CHMODJOB_STATE_CHMODING:
0279         // qDebug() << "-> chmodNextFile";
0280         d->chmodNextFile();
0281         return;
0282     default:
0283         Q_ASSERT(false);
0284         return;
0285     }
0286 }
0287 
0288 ChmodJob *KIO::chmod(const KFileItemList &lstItems, int permissions, int mask, const QString &owner, const QString &group, bool recursive, JobFlags flags)
0289 {
0290     KUserId uid = KUserId::fromName(owner);
0291     KGroupId gid = KGroupId::fromName(group);
0292     return ChmodJobPrivate::newJob(lstItems, permissions, mask, uid, gid, recursive, flags);
0293 }
0294 
0295 #include "moc_chmodjob.cpp"